From 8735974ce36e716a23b6a2a5ed6d357a9eafbfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Mon, 22 Sep 2025 10:29:58 +0100 Subject: [PATCH 01/22] Update release workflow --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c915f1502a..0b9f5bc713 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -142,8 +142,8 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: roda-ui/roda-wui/target/roda-wui-${{ env.release_version }}.war - asset_name: roda-wui-${{ env.release_version }}.war + file: roda-ui/roda-wui/target/roda-wui-${{ env.release_version }}.jar + asset_name: roda-wui-${{ env.release_version }}.jar tag: ${{ github.ref }} release_name: ${{ github.ref_name }} draft: true From 68435604c75b6e2e38c8eb1b5173533803e05815 Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira Date: Fri, 3 Oct 2025 13:46:12 +0100 Subject: [PATCH 02/22] include file itself when fetching modifications under storagePath in findModificationsUnderStoragePath. --- .../transaction/TransactionalStoragePathRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roda-core/roda-core/src/main/java/org/roda/core/repository/transaction/TransactionalStoragePathRepository.java b/roda-core/roda-core/src/main/java/org/roda/core/repository/transaction/TransactionalStoragePathRepository.java index f98ebb5dbb..0c4291af74 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/repository/transaction/TransactionalStoragePathRepository.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/repository/transaction/TransactionalStoragePathRepository.java @@ -45,7 +45,7 @@ List findByTransactionLogAndOperationType( @Param("transactionLog") TransactionLog transactionLog, @Param("operationType") OperationType operationType, @Param("operationState") OperationState operationState); - @Query("SELECT o FROM TransactionalStoragePathOperationLog o WHERE o.transactionLog = :transactionLog AND o.storagePath LIKE CONCAT(:storagePath, '/%') AND o.operationType <> 'READ'") + @Query("SELECT o FROM TransactionalStoragePathOperationLog o WHERE o.transactionLog = :transactionLog AND (o.storagePath = :storagePath OR o.storagePath LIKE CONCAT(:storagePath, '/%')) AND o.operationType <> 'READ'") List findModificationsUnderStoragePath( @Param("transactionLog") TransactionLog transactionLog, @Param("storagePath") String storagePath); } From 2288edcdd9c421c8ba3d57ab685e083d0ea94344 Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira Date: Fri, 3 Oct 2025 13:53:41 +0100 Subject: [PATCH 03/22] changed LdapUtilityTestHelper bitnami image to bitnamilegacy --- .../main/java/org/roda/core/security/LdapUtilityTestHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/security/LdapUtilityTestHelper.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/security/LdapUtilityTestHelper.java index 68197c94d8..c24c2be83b 100644 --- a/roda-core/roda-core-tests/src/main/java/org/roda/core/security/LdapUtilityTestHelper.java +++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/security/LdapUtilityTestHelper.java @@ -38,7 +38,7 @@ public LdapUtility getLdapUtility() { public LdapUtilityTestHelper() { final String ldapBaseDN = "dc=roda,dc=org"; - DockerImageName OPENLDAP_IMAGE = DockerImageName.parse("docker.io/bitnami/openldap:2.6"); + DockerImageName OPENLDAP_IMAGE = DockerImageName.parse("docker.io/bitnamilegacy/openldap:2.6"); openldap = new GenericContainer<>(OPENLDAP_IMAGE); openldap.withExposedPorts(1389); From 91315c99fb5f2cdfc05b41e608c6f536a2e3f06d Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira Date: Fri, 3 Oct 2025 13:57:38 +0100 Subject: [PATCH 04/22] changed bitnami image name to bitnamilegacy in development.yml . --- .github/workflows/development.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index ee18b8ae3e..e369c94268 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -36,7 +36,7 @@ jobs: POSTGRES_PASSWORD: roda POSTGRES_DB: roda_core_db openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 ports: - 1389:1389 - 1636:1636 From d651101b3c6eac79a9385e45b49fbc6e1b401e6a Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira Date: Fri, 7 Nov 2025 16:04:38 +0000 Subject: [PATCH 05/22] refactored copy method that received toPath argument to export to file system. --- .../main/java/org/roda/core/storage/fs/FileStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java index 455287745f..02226c0103 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java @@ -568,7 +568,7 @@ public void copy(StorageService fromService, StoragePath fromStoragePath, Storag @Override public void export(StorageService fromService, StoragePath fromStoragePath, Path toPath, String resource, - boolean replaceExisting) throws AlreadyExistsException, GenericException { + boolean replaceExisting) throws AlreadyExistsException, GenericException { Path sourcePath = null; if (StringUtils.isNotBlank(resource)) { sourcePath = FSUtils.getEntityPath(basePath, fromStoragePath).resolve(resource); From db9be08cb6cde7211b6b32ad2dab74c159501070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Mon, 10 Nov 2025 10:06:10 +0000 Subject: [PATCH 06/22] Add UTF-8 encoding when downloading files with non-ASCII characteres --- deploys/standalone/docker-compose-dev.yaml | 2 +- .../org/roda/wui/api/v2/utils/ApiUtils.java | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/deploys/standalone/docker-compose-dev.yaml b/deploys/standalone/docker-compose-dev.yaml index b8aeb079ce..ecd695b152 100644 --- a/deploys/standalone/docker-compose-dev.yaml +++ b/deploys/standalone/docker-compose-dev.yaml @@ -79,7 +79,7 @@ services: - "8025:8025" # web ui openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 container_name: openldap restart: unless-stopped user: 1001:root diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/utils/ApiUtils.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/utils/ApiUtils.java index e6b50f8010..aab0c6a4c1 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/utils/ApiUtils.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/utils/ApiUtils.java @@ -7,11 +7,12 @@ */ package org.roda.wui.api.v2.utils; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Date; import java.util.concurrent.TimeUnit; -import org.roda.core.RodaCoreFactory; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.NotFoundException; @@ -22,7 +23,6 @@ import org.roda.core.data.v2.StreamResponse; import org.roda.core.data.v2.common.Pair; import org.roda.core.model.ModelService; -import org.roda.core.storage.BinaryConsumesOutputStream; import org.roda.core.storage.RangeConsumesOutputStream; import org.roda.wui.common.model.RequestContext; import org.springframework.http.CacheControl; @@ -48,8 +48,8 @@ public static ResponseEntity okResponse(StreamResponse st StreamingResponseBody responseStream = outputStream -> streamResponse.getStream().consumeOutputStream(outputStream); responseHeaders.add("Content-Type", streamResponse.getStream().getMediaType()); - responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + streamResponse.getStream().getFileName() + "\""); + responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + + URLEncoder.encode(streamResponse.getStream().getFileName(), StandardCharsets.UTF_8) + "\""); responseHeaders.add("Content-Length", String.valueOf(streamResponse.getStream().getSize())); Date lastModifiedDate = streamResponse.getStream().getLastModified(); @@ -71,8 +71,8 @@ public static ResponseEntity rangeResponse(HttpHeaders he if (headers.getRange().isEmpty()) { responseStream = consumesOutputStream::consumeOutputStream; responseHeaders.add("Content-Type", consumesOutputStream.getMediaType()); - responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename=\"" + consumesOutputStream.getFileName() + "\""); + responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + + URLEncoder.encode(consumesOutputStream.getFileName(), StandardCharsets.UTF_8) + "\""); responseHeaders.add("Content-Length", String.valueOf(consumesOutputStream.getSize())); return ResponseEntity.ok().headers(responseHeaders).body(responseStream); @@ -86,7 +86,7 @@ public static ResponseEntity rangeResponse(HttpHeaders he responseHeaders.add(HttpHeaders.CONTENT_TYPE, consumesOutputStream.getMediaType()); responseHeaders.add(HttpHeaders.CONTENT_LENGTH, contentLength); responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, - "inline; filename=\"" + consumesOutputStream.getFileName() + "\""); + "inline; filename=\"" + URLEncoder.encode(consumesOutputStream.getFileName(), StandardCharsets.UTF_8) + "\""); responseHeaders.add(HttpHeaders.ACCEPT_RANGES, "bytes"); responseHeaders.add(HttpHeaders.CONTENT_RANGE, "bytes" + " " + start + "-" + end + "/" + consumesOutputStream.getSize()); @@ -105,26 +105,26 @@ public static ResponseEntity rangeResponse(HttpHeaders he } public static StreamResponse download(RequestContext requestContext, IsRODAObject object, String... pathPartials) - throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { + throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { return download(requestContext, object, null, false, pathPartials); } public static StreamResponse download(RequestContext requestContext, LiteRODAObject lite, String... pathPartials) - throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { - return download(requestContext,lite, null, false, pathPartials); + throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { + return download(requestContext, lite, null, false, pathPartials); } - public static StreamResponse download(RequestContext requestContext, IsRODAObject object, String fileName, boolean addTopDirectory, - String... pathPartials) - throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { + public static StreamResponse download(RequestContext requestContext, IsRODAObject object, String fileName, + boolean addTopDirectory, String... pathPartials) + throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { ModelService model = requestContext.getModelService(); ConsumesOutputStream download = model.exportObjectToStream(object, fileName, addTopDirectory, pathPartials); return new StreamResponse(download); } - public static StreamResponse download(RequestContext requestContext, LiteRODAObject lite, String fileName, boolean addTopDirectory, - String... pathPartials) - throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { + public static StreamResponse download(RequestContext requestContext, LiteRODAObject lite, String fileName, + boolean addTopDirectory, String... pathPartials) + throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { ModelService model = requestContext.getModelService(); ConsumesOutputStream download = model.exportObjectToStream(lite, fileName, addTopDirectory, pathPartials); return new StreamResponse(download); @@ -136,8 +136,8 @@ public static StreamResponse download(RequestContext requestContext, LiteRODAObj * values are provided. */ public static Pair processPagingParams(String start, String limit) { - Integer startInteger; - Integer limitInteger; + int startInteger; + int limitInteger; try { startInteger = Integer.parseInt(start); if (startInteger < 0) { From 47688ded468843b59177792d9b01493803020eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Mon, 10 Nov 2025 10:07:44 +0000 Subject: [PATCH 07/22] Update EARK SIP 2 plugin to support representation status other than Original --- .../core/plugins/base/ingest/EARKSIP2ToAIPPluginUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/EARKSIP2ToAIPPluginUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/EARKSIP2ToAIPPluginUtils.java index 3e056fbb67..a2082b3903 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/EARKSIP2ToAIPPluginUtils.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/EARKSIP2ToAIPPluginUtils.java @@ -9,6 +9,8 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -307,7 +309,7 @@ private static void processIPRepresentationInformation(ModelService model, IPRep // Either we're not updating or the retrieve failed if (representation == null) { representation = model.createRepresentation(aipId, sr.getObjectID(), isOriginal, representationType, notify, - username); + username, isOriginal ? Collections.emptyList() : List.of(sr.getStatus().asString())); if (reportItem != null && update) { reportItem.getSipInformation().addRepresentationData(aipId, IdUtils.getRepresentationId(representation)); } From 3c996dd3a2a6c59f6cda9cf0565878e965953e4f Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira Date: Fri, 26 Sep 2025 16:47:48 +0100 Subject: [PATCH 08/22] fixed download api and RestUtils methods for file/folder preview and download. --- .../roda/core/data/common/RodaConstants.java | 3 + .../resources/config/roda-roles.properties | 4 ++ .../wui/api/v2/controller/DIPController.java | 4 +- .../api/v2/controller/DIPFileController.java | 58 ++++++++++++++++++ .../wui/api/v2/services/DIPFileService.java | 60 +++++++++++++++++++ .../wui/api/v2/services/FilesService.java | 8 ++- .../org/roda/wui/client/browse/BrowseDIP.java | 24 ++++++-- .../roda/wui/client/browse/BrowseDIP.ui.xml | 2 +- .../wui/client/browse/DipFilePreview.java | 8 +-- .../wui/client/browse/IndexedFilePreview.java | 2 +- .../wui/client/browse/tabs/BrowseDIPTabs.java | 2 +- .../common/BrowseDIPFileActionsToolbar.java | 36 +++++++++++ .../wui/common/client/tools/RestUtils.java | 41 +++++++------ 13 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DIPFileService.java create mode 100644 roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/BrowseDIPFileActionsToolbar.java diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/common/RodaConstants.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/common/RodaConstants.java index d394118bab..613e122d59 100644 --- a/roda-common/roda-common-data/src/main/java/org/roda/core/data/common/RodaConstants.java +++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/common/RodaConstants.java @@ -401,6 +401,9 @@ public enum DateGranularity { // dips public static final String API_REST_V2_DIPS = "api/v2/dips/"; + // dipfiles + public static final String API_REST_V2_DIPFILES = "api/v2/dip-files/"; + // representation-information public static final String API_REST_V2_REPRESENTATION_INFORMATION = "api/v2/representation-information/"; diff --git a/roda-core/roda-core/src/main/resources/config/roda-roles.properties b/roda-core/roda-core/src/main/resources/config/roda-roles.properties index c740bad3c2..0cb9043586 100644 --- a/roda-core/roda-core/src/main/resources/config/roda-roles.properties +++ b/roda-core/roda-core/src/main/resources/config/roda-roles.properties @@ -252,6 +252,10 @@ core.roles.org.roda.wui.api.v2.controller.DIPController.deleteIndexedDIPs = aip. core.roles.org.roda.wui.api.v2.controller.DIPController.downloadBinary = aip.read core.roles.org.roda.wui.api.v2.controller.DIPController.updatePermissions = aip.update +# DIPFile roles +core.roles.org.roda.wui.api.v2.controller.DIPFileController.previewBinary = aip.read +core.roles.org.roda.wui.api.v2.controller.DIPFileController.downloadBinary = aip.read + # Configuration roles core.roles.org.roda.wui.api.v2.controller.ConfigurationController.retrievePluginsInfo = job.read core.roles.org.roda.wui.api.v2.controller.ConfigurationController.retrieveReindexPluginObjectClasses = job.read diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPController.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPController.java index b9774ed312..887a3238b0 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPController.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPController.java @@ -107,10 +107,8 @@ public ResponseEntity downloadBinary( public ResponseEntity process(RequestContext requestContext, RequestControllerAssistant controllerAssistant) throws RODAException, RESTException { - IndexedDIP dip = indexService.retrieve(IndexedDIP.class, dipUUID, new ArrayList<>()); - + IndexedDIP dip = requestContext.getIndexService().retrieve(IndexedDIP.class, dipUUID, new ArrayList<>()); controllerAssistant.checkObjectPermissions(requestContext.getUser(), dip); - controllerAssistant.setParameters(RodaConstants.CONTROLLER_DIP_UUID_PARAM, dipUUID); return ApiUtils.okResponse(dipService.createStreamResponse(requestContext, dip.getUUID())); diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPFileController.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPFileController.java index 5f963975b5..a069081f60 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPFileController.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DIPFileController.java @@ -11,6 +11,7 @@ import java.util.List; import org.roda.core.data.common.RodaConstants; +import org.roda.core.data.exceptions.RODAException; import org.roda.core.data.v2.generics.LongResponse; import org.roda.core.data.v2.index.CountRequest; import org.roda.core.data.v2.index.FindRequest; @@ -18,18 +19,32 @@ import org.roda.core.data.v2.index.SuggestRequest; import org.roda.core.data.v2.ip.DIPFile; import org.roda.core.model.utils.UserUtility; +import org.roda.wui.api.v2.exceptions.RESTException; +import org.roda.wui.api.v2.exceptions.model.ErrorResponseMessage; +import org.roda.wui.api.v2.services.DIPFileService; import org.roda.wui.api.v2.services.IndexService; import org.roda.wui.api.v2.utils.ApiUtils; import org.roda.wui.client.services.DIPFileRestService; +import org.roda.wui.common.RequestControllerAssistant; import org.roda.wui.common.model.RequestContext; import org.roda.wui.common.utils.RequestUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.servlet.http.HttpServletRequest; /** @@ -44,6 +59,12 @@ public class DIPFileController implements DIPFileRestService, Exportable { @Autowired IndexService indexService; + @Autowired + DIPFileService dipFileService; + + @Autowired + RequestHandler requestHandler; + @Override public DIPFile findByUuid(String uuid, String localeString) { return indexService.retrieve(DIPFile.class, uuid, new ArrayList<>()); @@ -75,4 +96,41 @@ public ResponseEntity exportToCSV(String findRequestStrin // delegate return ApiUtils.okResponse(indexService.exportToCSV(findRequestString, DIPFile.class)); } + + @GetMapping(path = "{uuid}/preview", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @Operation(summary = "DIP file View", description = "View DIP file binary", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StreamingResponseBody.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class))), + @ApiResponse(responseCode = "404", description = "DIP file not found", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class)))}) + public ResponseEntity previewBinary( + @Parameter(description = "The UUID of the existing DIP file", required = true) @PathVariable(name = "uuid") String dipFileUUID, + @RequestHeader HttpHeaders headers) { + return requestHandler.processRequest(new RequestHandler.RequestProcessor>() { + @Override + public ResponseEntity process(RequestContext requestContext, + RequestControllerAssistant controllerAssistant) throws RODAException, RESTException { + List dipPermissionFields = new ArrayList<>(RodaConstants.DIPFILE_FIELDS_TO_RETURN); + DIPFile dipFile = requestContext.getIndexService().retrieve(DIPFile.class, dipFileUUID, dipPermissionFields); + return ApiUtils.rangeResponse(headers, dipFileService.retrieveDIPFileRangeStream(requestContext, dipFile)); + } + }); + } + + @GetMapping(path = "{uuid}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) + @Operation(summary = "DIP file Preview", description = "Preview the DIP file binary", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = StreamingResponseBody.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class))), + @ApiResponse(responseCode = "404", description = "DIP file not found", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class)))}) + public ResponseEntity downloadBinary(@PathVariable(name = "uuid") String dipFileUUID) { + + return requestHandler.processRequest(new RequestHandler.RequestProcessor>() { + @Override + public ResponseEntity process(RequestContext requestContext, + RequestControllerAssistant controllerAssistant) throws RODAException, RESTException { + List fileFields = new ArrayList<>(RodaConstants.DIPFILE_FIELDS_TO_RETURN); + DIPFile dipFile = requestContext.getIndexService().retrieve(DIPFile.class, dipFileUUID, fileFields); + return ApiUtils.okResponse(dipFileService.retrieveDIPFileStreamResponse(requestContext, dipFile)); + } + }); + } } diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DIPFileService.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DIPFileService.java new file mode 100644 index 0000000000..1da4450c13 --- /dev/null +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DIPFileService.java @@ -0,0 +1,60 @@ +package org.roda.wui.api.v2.services; + +import org.roda.core.data.exceptions.AuthorizationDeniedException; +import org.roda.core.data.exceptions.GenericException; +import org.roda.core.data.exceptions.NotFoundException; +import org.roda.core.data.exceptions.RequestNotValidException; +import org.roda.core.data.v2.ConsumesOutputStream; +import org.roda.core.data.v2.LiteRODAObject; +import org.roda.core.data.v2.StreamResponse; +import org.roda.core.data.v2.ip.DIPFile; +import org.roda.core.model.LiteRODAObjectFactory; +import org.roda.core.model.ModelService; +import org.roda.core.storage.DirectResourceAccess; +import org.roda.core.storage.RangeConsumesOutputStream; +import org.roda.wui.common.model.RequestContext; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * + * @author Eduardo Teixeira + */ +@Service +public class DIPFileService { + public RangeConsumesOutputStream retrieveDIPFileRangeStream(RequestContext requestContext, DIPFile dipfile) + throws RequestNotValidException { + ModelService model = requestContext.getModelService(); + if (!dipfile.isDirectory()) { + final RangeConsumesOutputStream stream; + try { + DirectResourceAccess directDIPFileAccess = model.getDirectAccess(dipfile); + stream = new RangeConsumesOutputStream(directDIPFileAccess.getPath()); + return stream; + } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException e) { + throw new RuntimeException(e); + } + + } else + throw new RequestNotValidException("Range stream for directory unsupported"); + } + + public StreamResponse retrieveDIPFileStreamResponse(RequestContext requestContext, DIPFile dipFile) + throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { + ModelService model = requestContext.getModelService(); + + final ConsumesOutputStream stream; + List idPaths = new ArrayList<>(); + idPaths.add(dipFile.getDipId()); + idPaths.addAll(dipFile.getPath()); + idPaths.add(dipFile.getId()); + + Optional rodaDIPobj = LiteRODAObjectFactory.get(DIPFile.class, idPaths.toArray(String[]::new)); + stream = model.exportObjectToStream(rodaDIPobj.get()); + return new StreamResponse(stream); + + } +} diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java index cc38198891..502f8c3188 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java @@ -247,7 +247,13 @@ public RangeConsumesOutputStream retrieveAIPRepresentationRangeStream(RequestCon public StreamResponse retrieveAIPRepresentationFile(RequestContext requestContext, IndexedFile indexedFile) throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { ModelService model = requestContext.getModelService(); - Optional liteFile = LiteRODAObjectFactory.get(File.class, indexedFile.getId()); + List ids = new ArrayList<>(); + ids.add(indexedFile.getAipId()); + ids.add(indexedFile.getRepresentationId()); + ids.addAll(indexedFile.getPath()); + ids.add(indexedFile.getId()); + Optional liteFile = LiteRODAObjectFactory.get(File.class, ids.toArray(String[]::new)); + if (liteFile.isEmpty()) { throw new RequestNotValidException("Couldn't retrieve file with id: " + indexedFile.getId()); } diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.java index 0462cb705c..1ca05ab09f 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.java @@ -36,7 +36,10 @@ import org.roda.core.data.v2.ip.IndexedRepresentation; import org.roda.core.data.v2.ip.RepresentationLink; import org.roda.wui.client.browse.tabs.BrowseDIPTabs; +import org.roda.wui.client.common.ActionsToolbar; import org.roda.wui.client.common.BrowseDIPActionsToolbar; +import org.roda.wui.client.common.BrowseDIPFileActionsToolbar; +import org.roda.wui.client.common.BrowseObjectActionsToolbar; import org.roda.wui.client.common.NavigationToolbar; import org.roda.wui.client.common.NoAsyncCallback; import org.roda.wui.client.common.UserLogin; @@ -269,6 +272,8 @@ private void applyWhenIndexedFile(Services services, String historyDipUUID, Stri .fileResource(s -> s.retrieveIndexedFileViaRequest(request)); CompletableFuture indexedAIPCompletableFuture = services .aipResource(s -> s.findByUuid(request.getAipId(), LocaleInfo.getCurrentLocale().getLocaleName())); + CompletableFuture indexedRepresentationCompletableFuture = services + .representationResource(s -> s.retrieveIndexedRepresentationViaRequest(request)); CompletableFuture showEmbeddedDIPFuture = services .configurationsResource(ConfigurationRestService::retrieveShowEmbeddedDIP).exceptionally(throwable1 -> false); @@ -279,11 +284,13 @@ private void applyWhenIndexedFile(Services services, String historyDipUUID, Stri indexedFileCompletableFuture, showEmbeddedDIPFuture).thenApply(v -> { BrowseDIPResponse response = new BrowseDIPResponse(); IndexedAIP indexedAIP = indexedAIPCompletableFuture.join(); + IndexedRepresentation indexedRepresentation = indexedRepresentationCompletableFuture.join(); IndexedFile indexedFile = indexedFileCompletableFuture.join(); IndexResult dipFileIndexResult = retrieveDIPFileCompletableFuture.join(); response.setIndexedAIP(indexedAIP); response.setPermissions(indexedAIP.getPermissions()); + response.setIndexedRepresentation(indexedRepresentation); response.setIndexedFile(indexedFile); response.setReferred(indexedFile); response.setDip(indexedDIP); @@ -331,8 +338,8 @@ private void errorRedirect(AsyncCallback callback) { FlowPanel container; @UiField NavigationToolbar navigationToolbar; - @UiField - BrowseDIPActionsToolbar objectToolbar; + @UiField(provided = true) + ActionsToolbar objectToolbar; @UiField BrowseDIPTabs browseTab; @@ -351,6 +358,17 @@ public BrowseDIP(Viewers viewers, BrowseDIPResponse response, Services services) IndexedDIP dip = response.getDip(); DIPFile dipFile = response.getDipFile(); + if (dipFile != null) { + BrowseObjectActionsToolbar toolbar = new BrowseDIPFileActionsToolbar(); + toolbar.setObjectAndBuild(dipFile, response.getPermissions(), handler); + objectToolbar = toolbar; + + } else { + BrowseObjectActionsToolbar toolbar = new BrowseDIPActionsToolbar(); + toolbar.setObjectAndBuild(dip, dip.getPermissions(), handler); + objectToolbar = toolbar; + } + initWidget(uiBinder.createAndBindUi(this)); navigationToolbar.withObject(dipFile != null ? dipFile : dip); @@ -381,8 +399,6 @@ public BrowseDIP(Viewers viewers, BrowseDIPResponse response, Services services) navigationToolbar.withAlternativeStyle(true); } - objectToolbar.setObjectAndBuild(dip, dip.getPermissions(), handler); - browseTab.init(viewers, response, handler); keyboardFocus.setFocus(true); diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.ui.xml b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.ui.xml index a8b31cd583..d76caf14d4 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.ui.xml +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/BrowseDIP.ui.xml @@ -10,7 +10,7 @@ - + diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/DipFilePreview.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/DipFilePreview.java index 1e2fb501ac..06e60546f8 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/DipFilePreview.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/DipFilePreview.java @@ -19,7 +19,6 @@ import org.roda.wui.common.client.tools.RestUtils; import com.google.gwt.core.client.GWT; -import com.google.gwt.user.client.Command; import com.google.gwt.user.client.ui.Widget; import config.i18n.client.ClientMessages; @@ -32,15 +31,10 @@ public class DipFilePreview extends BitstreamPreview { private static final ClientMessages messages = GWT.create(ClientMessages.class); public DipFilePreview(Viewers viewers, DIPFile dipFile) { - super(viewers, RestUtils.createDipFileDownloadUri(dipFile.getUUID(), CONTENT_DISPOSITION_INLINE), NO_FORMAT, + super(viewers, RestUtils.createDipFilePreviewUri(dipFile.getUUID(), CONTENT_DISPOSITION_INLINE), NO_FORMAT, dipFile.getId(), dipFile.getSize(), dipFile.isDirectory(), dipFile); } - public DipFilePreview(Viewers viewers, DIPFile dipFile, Command onPreviewFailure) { - super(viewers, RestUtils.createDipFileDownloadUri(dipFile.getUUID(), CONTENT_DISPOSITION_INLINE), NO_FORMAT, - dipFile.getId(), dipFile.getSize(), dipFile.isDirectory(), onPreviewFailure, dipFile); - } - @Override protected Widget directoryPreview() { final Filter filter = new Filter( diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/IndexedFilePreview.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/IndexedFilePreview.java index b16b9e3843..171fd97f20 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/IndexedFilePreview.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/IndexedFilePreview.java @@ -40,7 +40,7 @@ public class IndexedFilePreview extends BitstreamPreview { public IndexedFilePreview(Viewers viewers, IndexedFile file, boolean isAvailable, boolean justActive, AIPState state, Permissions permissions, Command onPreviewFailure) { - super(viewers, RestUtils.createRepresentationFileDownloadUri(file.getUUID(), CONTENT_DISPOSITION_INLINE), + super(viewers, RestUtils.createRepresentationFilePreviewUri(file.getUUID(), CONTENT_DISPOSITION_INLINE), file.getFileFormat(), file.getOriginalName() != null ? file.getOriginalName() : file.getId(), file.getSize(), file.isDirectory(), isAvailable, onPreviewFailure, file, justActive, state, permissions); } diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/tabs/BrowseDIPTabs.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/tabs/BrowseDIPTabs.java index ed937ca52f..f204043980 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/tabs/BrowseDIPTabs.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/browse/tabs/BrowseDIPTabs.java @@ -47,7 +47,7 @@ public void init(Viewers viewers, BrowseDIPResponse browseDIPResponse, DIPFile dipFile = browseDIPResponse.getDipFile(); // DIPFile preview - createAndAddTab(SafeHtmlUtils.fromSafeConstant(messages.descriptiveMetadataTab()), new TabContentBuilder() { + createAndAddTab(SafeHtmlUtils.fromSafeConstant(messages.viewTab()), new TabContentBuilder() { @Override public Widget buildTabWidget() { if (dipFile != null) { diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/BrowseDIPFileActionsToolbar.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/BrowseDIPFileActionsToolbar.java new file mode 100644 index 0000000000..666a3122a1 --- /dev/null +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/client/common/BrowseDIPFileActionsToolbar.java @@ -0,0 +1,36 @@ +package org.roda.wui.client.common; + +import java.util.ArrayList; +import java.util.List; + +import org.roda.core.data.common.RodaConstants; +import org.roda.core.data.v2.ip.DIPFile; +import org.roda.wui.client.common.actions.DisseminationFileActions; +import org.roda.wui.client.common.actions.model.ActionableObject; +import org.roda.wui.client.common.actions.widgets.ActionableWidgetBuilder; +import org.roda.wui.common.client.tools.ConfigurationManager; + +/** + * + * @author Eduardo Teixeira + */ +public class BrowseDIPFileActionsToolbar extends BrowseObjectActionsToolbar { + public void buildIcon() { + setIcon(ConfigurationManager.getString(RodaConstants.UI_ICONS_CLASS, DIPFile.class.getSimpleName())); + } + + public void buildTags() { + // do nothing + } + + public void buildActions() { + this.actions.clear(); + // AIP actions + DisseminationFileActions dipFileActions; + dipFileActions = DisseminationFileActions.get(actionPermissions); + this.actions.add(new ActionableWidgetBuilder(dipFileActions).withActionCallback(actionCallback) + .buildGroupedListWithObjects(new ActionableObject<>(object), new ArrayList<>(), + List.of(DisseminationFileActions.DisseminationFileAction.DOWNLOAD))); + + } +} diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/common/client/tools/RestUtils.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/common/client/tools/RestUtils.java index 69c9ac3925..7221fbc2fc 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/common/client/tools/RestUtils.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/common/client/tools/RestUtils.java @@ -84,11 +84,7 @@ public static SafeUri createRepresentationOtherMetadataDownloadUri(String aipId, return UriUtils.fromSafeConstant(b.toString()); } - public static SafeUri createRepresentationFileDownloadUri(String fileUuid) { - return createRepresentationFileDownloadUri(fileUuid, false); - } - - public static SafeUri createRepresentationFileDownloadUri(String fileUuid, boolean contentDispositionInline) { + public static SafeUri createRepresentationFilePreviewUri(String fileUuid, boolean contentDispositionInline) { // api/v2/files/{file_uuid}/preview String b = RodaConstants.API_REST_V2_FILES + URL.encodeQueryString(fileUuid) + RodaConstants.API_REST_V2_PREVIEW_HANDLER; @@ -96,6 +92,14 @@ public static SafeUri createRepresentationFileDownloadUri(String fileUuid, boole return UriUtils.fromSafeConstant(b); } + public static SafeUri createRepresentationFileDownloadUri(String fileUuid){ + // api/v2/files/{file_uuid}/download + StringBuilder b = new StringBuilder(); + b.append(RodaConstants.API_REST_V2_FILES).append(URL.encodeQueryString(fileUuid)).append(RodaConstants.API_REST_V2_DOWNLOAD_HANDLER); + + return UriUtils.fromSafeConstant(b.toString()); + } + public static SafeUri createDipDownloadUri(String dipUUID) { // api/v2/dips/{uuid}/download StringBuilder b = new StringBuilder(); @@ -105,23 +109,22 @@ public static SafeUri createDipDownloadUri(String dipUUID) { return UriUtils.fromSafeConstant(b.toString()); } - public static SafeUri createDipFileDownloadUri(String dipFileUUID) { - return createDipFileDownloadUri(dipFileUUID, false); - } - - public static SafeUri createDipFileDownloadUri(String dipFileUUID, boolean contentDispositionInline) { - - // api/v1/dipfiles/{file_uuid}?acceptFormat=bin&inline={inline} + public static SafeUri createDipFilePreviewUri(String dipFileUUID, boolean contentDispositionInline) { + // api/v2/dip-files/{file_uuid}/preview?inline={inline} StringBuilder b = new StringBuilder(); // base uri - b.append(RodaConstants.API_REST_V1_DIPFILES).append(URL.encodeQueryString(dipFileUUID)); - // accept format attribute - b.append(RodaConstants.API_QUERY_START).append(RodaConstants.API_QUERY_KEY_ACCEPT_FORMAT) - .append(RodaConstants.API_QUERY_ASSIGN_SYMBOL).append(RodaConstants.API_QUERY_VALUE_ACCEPT_FORMAT_BIN); - - b.append(RodaConstants.API_QUERY_SEP).append(RodaConstants.API_QUERY_KEY_INLINE) - .append(RodaConstants.API_QUERY_ASSIGN_SYMBOL).append(contentDispositionInline); + b.append(RodaConstants.API_REST_V2_DIPFILES).append(URL.encodeQueryString(dipFileUUID)) + .append(RodaConstants.API_REST_V2_PREVIEW_HANDLER).append(RodaConstants.API_QUERY_START) + .append(RodaConstants.API_QUERY_KEY_INLINE).append(RodaConstants.API_QUERY_ASSIGN_SYMBOL) + .append(contentDispositionInline); + return UriUtils.fromSafeConstant(b.toString()); + } + public static SafeUri createDipFileDownloadUri(String dipFileUUID){ + // api/v2/dip-files/{file_uuid}/download + StringBuilder b = new StringBuilder(); + b.append(RodaConstants.API_REST_V2_DIPFILES).append(URL.encodeQueryString(dipFileUUID)) + .append(RodaConstants.API_REST_V2_DOWNLOAD_HANDLER); return UriUtils.fromSafeConstant(b.toString()); } From 128ac2998746da72c9c32a18e2005d75acf9614e Mon Sep 17 00:00:00 2001 From: Gabriel Barros Date: Thu, 2 Oct 2025 10:24:48 +0100 Subject: [PATCH 09/22] Updates bitnami image to legacy for test only --- .github/workflows/staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 7593e4edf3..d4c70baa07 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -36,7 +36,7 @@ jobs: ports: - 1025:1025 openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 ports: - 1389:1389 - 1636:1636 From 413c5cb21d5acb2748276eb2c4ce0602a35c9f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Mon, 10 Nov 2025 10:13:09 +0000 Subject: [PATCH 10/22] Update bitnami image in different workflows --- .github/workflows/CI.yml | 2 +- .github/workflows/latest.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ce99efb778..591217510d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,7 +37,7 @@ jobs: ports: - 1025:1025 openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 ports: - 1389:1389 - 1636:1636 diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index 9d793515aa..287fa755a6 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -36,7 +36,7 @@ jobs: ports: - 1025:1025 openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 ports: - 1389:1389 - 1636:1636 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b9f5bc713..ec256989c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: ports: - 1025:1025 openldap: - image: docker.io/bitnami/openldap:2.6 + image: docker.io/bitnamilegacy/openldap:2.6 ports: - 1389:1389 - 1636:1636 From 070c2a8ab9b1dc8783aeab16fcbdee3dc7fab502 Mon Sep 17 00:00:00 2001 From: Alexandre Flores Date: Fri, 28 Nov 2025 10:20:06 +0000 Subject: [PATCH 11/22] OnOfFilter retries init if filter enabled config is missing --- .../java/org/roda/wui/filter/OnOffFilter.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/OnOffFilter.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/OnOffFilter.java index 3d958437db..b6d5819fbf 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/OnOffFilter.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/filter/OnOffFilter.java @@ -14,6 +14,12 @@ import java.util.Iterator; import java.util.List; +import org.apache.commons.configuration.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.roda.core.RodaCoreFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; @@ -22,12 +28,6 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; -import org.apache.commons.configuration.Configuration; -import org.apache.commons.lang3.StringUtils; -import org.roda.core.RodaCoreFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * A filter that can be turned on/off using RODA configuration file. */ @@ -116,13 +116,14 @@ private boolean isConfigAvailable() { */ private void initInnerFilter() throws ServletException { final Configuration rodaConfig = RodaCoreFactory.getRodaConfiguration(); - if (rodaConfig == null) { + final String innerFilterClass = this.webXmlFilterConfig.getInitParameter(PARAM_INNER_FILTER_CLASS); + final String configPrefix = this.webXmlFilterConfig.getInitParameter(PARAM_CONFIG_PREFIX); + final String configKey = configPrefix + ".enabled"; + if (rodaConfig == null || !rodaConfig.containsKey(configKey)) { LOGGER.info("RODA configuration not available yet. Delaying init of {}.", this.webXmlFilterConfig.getInitParameter(PARAM_INNER_FILTER_CLASS)); } else { - final String innerFilterClass = this.webXmlFilterConfig.getInitParameter(PARAM_INNER_FILTER_CLASS); - final String configPrefix = this.webXmlFilterConfig.getInitParameter(PARAM_CONFIG_PREFIX); - if (rodaConfig.getBoolean(configPrefix + ".enabled", false)) { + if (rodaConfig.getBoolean(configKey, false)) { try { this.innerFilter = (Filter) Class.forName(innerFilterClass).newInstance(); this.innerFilter.init(getFilterConfig()); @@ -134,8 +135,8 @@ private void initInnerFilter() throws ServletException { } else { this.isOn = false; } + LOGGER.info("{} is {}", getFilterConfig().getFilterName(), (this.isOn ? "ON" : "OFF")); } - LOGGER.info("{} is {}", getFilterConfig().getFilterName(), (this.isOn ? "ON" : "OFF")); } /** From c674a116dd29fc39fa7574e0ffe8d6cb4a5ff331 Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira <58005905+eduardojst10@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:24:24 +0000 Subject: [PATCH 12/22] added bearer authentication get and delete method to HTTPUtils. (#3563) --- .../java/org/roda/core/util/HTTPUtility.java | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/roda-common/roda-common-utils/src/main/java/org/roda/core/util/HTTPUtility.java b/roda-common/roda-common-utils/src/main/java/org/roda/core/util/HTTPUtility.java index 96d4a03089..fc4e8c4d17 100644 --- a/roda-common/roda-common-utils/src/main/java/org/roda/core/util/HTTPUtility.java +++ b/roda-common/roda-common-utils/src/main/java/org/roda/core/util/HTTPUtility.java @@ -32,39 +32,41 @@ private HTTPUtility() { // do nothing } + public static String doMethodBearer(String url, String method, Optional token) throws GenericException { + try { + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestProperty("Accept", "application/json"); + con.setRequestProperty("Content-Type", "application/json"); + con.setRequestMethod(method); + addBearerAuthToConnection(con, token); + return executeRequest(con); + } catch (IOException e) { + throw new GenericException("Unable to connect to server", e); + } + } + public static String doMethod(String url, String method, Optional> basicAuth) throws GenericException { - String res = null; try { URL obj = new URL(url); HttpURLConnection con = (HttpURLConnection) obj.openConnection(); con.setRequestMethod(method); addBasicAuthToConnection(con, basicAuth); - int responseCode = con.getResponseCode(); - if (responseCode == 200) { - InputStream is = con.getInputStream(); - BufferedReader in = new BufferedReader(new InputStreamReader(is)); - String inputLine; - StringBuilder response = new StringBuilder(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); - } - in.close(); - is.close(); - res = response.toString(); - } else { - throw new GenericException("Unable to connect to server, response code: " + responseCode); - } + return executeRequest(con); } catch (IOException e) { throw new GenericException("Unable to connect to server", e); } - return res; } public static String doGet(String url) throws GenericException { return doMethod(url, METHOD_GET, Optional.empty()); } + public static String doGetBearer(String url, Optional token) throws GenericException { + return doMethodBearer(url, METHOD_GET, token); + } + public static String doGet(String url, Optional> basicAuth) throws GenericException { return doMethod(url, METHOD_GET, basicAuth); } @@ -77,6 +79,10 @@ public static String doDelete(String url, Optional> basicAu return doMethod(url, METHOD_DELETE, basicAuth); } + public static String doDeleteBearer(String url, Optional token) throws GenericException { + return doMethodBearer(url, METHOD_DELETE, token); + } + private static void addBasicAuthToConnection(HttpURLConnection connection, Optional> credentials) { if (credentials.isPresent()) { @@ -85,4 +91,28 @@ private static void addBasicAuthToConnection(HttpURLConnection connection, connection.setRequestProperty("Authorization", "Basic " + encoded); } } + + private static void addBearerAuthToConnection(HttpURLConnection connection, Optional token) { + token.ifPresent(s -> connection.setRequestProperty("Authorization", "Bearer " + s)); + } + + private static String executeRequest(HttpURLConnection con) throws IOException, GenericException { + int resCode = con.getResponseCode(); + + if (resCode == 200) { + InputStream is = con.getInputStream(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(is))) { + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + return response.toString(); + } finally { + is.close(); + } + } else { + throw new GenericException("Unable to connect to server, response code: " + resCode); + } + } } From 08a197d5639d6b0c18a1324f0665a99af89b067b Mon Sep 17 00:00:00 2001 From: Eduardo Teixeira <58005905+eduardojst10@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:27:29 +0000 Subject: [PATCH 13/22] fixed loss of indexed Technical Metadata; prevent technical metadata file ID collision and fix technical metadata handling. Fixes #3521 (#3561) --- .../org/roda/core/data/utils/URNUtils.java | 2 +- .../main/java/org/roda/core/util/IdUtils.java | 6 +++ .../schema/collections/FileCollection.java | 7 ++- .../org/roda/core/index/utils/SolrUtils.java | 2 +- .../roda/core/model/DefaultModelService.java | 53 +++++++++++++++++-- .../DefaultTransactionalModelService.java | 34 ++++++++++++ .../core/model/LiteRODAObjectFactory.java | 17 ++++++ .../org/roda/core/model/ModelService.java | 7 +++ .../TransactionalModelOperationRegistry.java | 17 +++++- .../wui/api/v2/services/FilesService.java | 12 ++--- 10 files changed, 142 insertions(+), 15 deletions(-) diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/utils/URNUtils.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/utils/URNUtils.java index 6861161e6f..0a3e415902 100644 --- a/roda-common/roda-common-data/src/main/java/org/roda/core/data/utils/URNUtils.java +++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/utils/URNUtils.java @@ -51,7 +51,7 @@ public static String createRodaPreservationURN(PreservationMetadataType preserva } public static String createRodaTechnicalMetadataURN(String id, String instanceId, String technicalMetadataType) { - return getTechnicalMetadataPrefix(technicalMetadataType, instanceId) + id; + return getTechnicalMetadataPrefix(technicalMetadataType, instanceId) + id + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION; } public static String getTechnicalMetadataPrefix(String technicalMetadataType, String instanceId) { diff --git a/roda-common/roda-common-utils/src/main/java/org/roda/core/util/IdUtils.java b/roda-common/roda-common-utils/src/main/java/org/roda/core/util/IdUtils.java index 104f01c2e8..ef06a327b7 100644 --- a/roda-common/roda-common-utils/src/main/java/org/roda/core/util/IdUtils.java +++ b/roda-common/roda-common-utils/src/main/java/org/roda/core/util/IdUtils.java @@ -191,6 +191,12 @@ public static String getTransferredResourceUUID(Path relativeToBase) { return getTransferredResourceUUID(relativeToBase.toString()); } + public static String createTechnicalMetadataFileId(String fileId, List fileDirectoryPath){ + if(fileDirectoryPath.isEmpty()) return fileId; + String pathPrefix = String.join(ID_SEPARATOR, fileDirectoryPath); + return pathPrefix + ID_SEPARATOR + fileId; + } + public static String getTransferredResourceUUID(String relativeToBase) { return IdUtils.createUUID(relativeToBase); } diff --git a/roda-core/roda-core/src/main/java/org/roda/core/index/schema/collections/FileCollection.java b/roda-core/roda-core/src/main/java/org/roda/core/index/schema/collections/FileCollection.java index c5e1c9b34d..b9b6e6b60c 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/index/schema/collections/FileCollection.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/index/schema/collections/FileCollection.java @@ -195,8 +195,11 @@ public SolrInputDocument toSolrDocument(ModelService model, File file, IndexingA Long sizeInBytes = 0L; - SolrUtils.indexRepresentationTechnicalMetadata(model, - getRepresentationTechnicalMetadata(((Info) info).aip, file.getRepresentationId()), fileId, doc); + if (!file.isDirectory()) { + String techMdFileId = IdUtils.createTechnicalMetadataFileId(fileId, file.getPath()); + SolrUtils.indexRepresentationTechnicalMetadata(model, + getRepresentationTechnicalMetadata(((Info) info).aip, file.getRepresentationId()), techMdFileId, doc); + } // Add information from PREMIS Binary premisFile = getFilePremisFile(model, file); diff --git a/roda-core/roda-core/src/main/java/org/roda/core/index/utils/SolrUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/index/utils/SolrUtils.java index efd3877c8b..99425125dd 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/index/utils/SolrUtils.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/index/utils/SolrUtils.java @@ -1545,7 +1545,7 @@ public static void indexRepresentationTechnicalMetadata(ModelService model, StoragePath storagePath = ModelUtils.getTechnicalMetadataStoragePath(techMd.getAipId(), techMd.getRepresentationId(), Collections.singletonList(techMd.getType()), - urn + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION); + urn); Binary binary = model.getStorage().getBinary(storagePath); diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultModelService.java b/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultModelService.java index 347df8c33e..f461c2e783 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultModelService.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultModelService.java @@ -721,6 +721,12 @@ public Binary retrieveDescriptiveMetadataBinary(String aipId, String representat return storage.getBinary(binaryPath); } + public Binary retrieveTechnicalMetadataBinary(String aipId, String representationId, List fileDirectoryPath, String fileId) + throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { + StoragePath binaryPath = ModelUtils.getTechnicalMetadataStoragePath(aipId, representationId, fileDirectoryPath, fileId); + return storage.getBinary(binaryPath); + } + @Override public DescriptiveMetadata retrieveDescriptiveMetadata(String aipId, String descriptiveMetadataId) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { @@ -1790,20 +1796,59 @@ public void createTechnicalMetadata(String aipId, String representationId, Strin StoragePath binaryPath = ModelUtils.getTechnicalMetadataStoragePath(aipId, representationId, Collections.singletonList(metadataType), urn); storage.createBinary(binaryPath, payload, false); - TechnicalMetadata techMd = new TechnicalMetadata(metadataType, aipId, representationId, metadataType); - AIP updatedAIP = null; if (aipId != null) { AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId); - aip.addTechnicalMetadata(techMd); - updatedAIP = updateAIPMetadata(aip, createdBy); + addTechnicalMetadataToAIPMetadata(aip, representationId, metadataType, createdBy, notify); + } + } + + @Override + public void updateTechnicalMetadata(String aipId, String representationId, String metadataType, String fileId, + ContentPayload payload, String createdBy, boolean notify) + throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException { + RodaCoreFactory.checkIfWriteIsAllowedAndIfFalseThrowException(nodeType); + + String urn = URNUtils.createRodaTechnicalMetadataURN(fileId, RODAInstanceUtils.getLocalInstanceIdentifier(), + metadataType.toLowerCase()); + StoragePath binaryPath = ModelUtils.getTechnicalMetadataStoragePath(aipId, representationId, + Collections.singletonList(metadataType), urn); + storage.updateBinaryContent(binaryPath, payload, false, false, true, null); + // update technicalmetadata logic + if (aipId != null) { + AIP aip = ResourceParseUtils.getAIPMetadata(getStorage(), aipId); + List techMetadataList = getTechnicalMetadata(aip, representationId); + techMetadataList.removeIf(tm -> tm.getId().equals(metadataType)); + addTechnicalMetadataToAIPMetadata(aip, representationId, metadataType, createdBy, notify); } + } + + private void addTechnicalMetadataToAIPMetadata(AIP aip, String representationId, String metadataType, + String createdBy, boolean notify) + throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { + TechnicalMetadata techMd = new TechnicalMetadata(metadataType, aip.getId(), representationId, metadataType); + + aip.addTechnicalMetadata(techMd); + AIP updatedAIP = updateAIPMetadata(aip, createdBy); if (notify && updatedAIP != null) { notifyAipUpdatedOnChanged(updatedAIP).failOnError(); } } + private List getTechnicalMetadata(AIP aip, String representationId) { + if (representationId == null) { + return new ArrayList<>(); + } + Optional oRep = aip.getRepresentations().stream() + .filter(rep -> rep.getId().equals(representationId)).findFirst(); + if (oRep.isPresent()) { + return oRep.get().getTechnicalMetadata(); + } + + return new ArrayList<>(); + } + @Override public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String aipId, String representationId, List fileDirectoryPath, String fileId, ContentPayload payload, String username, diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultTransactionalModelService.java b/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultTransactionalModelService.java index 4fa1334fd7..0d701343d3 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultTransactionalModelService.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/model/DefaultTransactionalModelService.java @@ -439,6 +439,23 @@ public Binary retrieveDescriptiveMetadataBinary(String aipId, String representat } } + @Override + public Binary retrieveTechnicalMetadataBinary(String aipId, String representationId, List fileDirectoryPath, + String fileId) throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException { + List operationLogs = operationRegistry.registerOperationForTechnicalMetadata(aipId, + representationId, fileDirectoryPath, fileId, OperationType.READ); + + try { + Binary binary = stagingModelService.retrieveTechnicalMetadataBinary(aipId, representationId, fileDirectoryPath, + fileId); + operationRegistry.updateOperationState(operationLogs, OperationState.SUCCESS); + return binary; + } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException e) { + operationRegistry.updateOperationState(operationLogs, OperationState.FAILURE); + throw e; + } + } + @Override public DescriptiveMetadata retrieveDescriptiveMetadata(String aipId, String descriptiveMetadataId) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException { @@ -1491,6 +1508,23 @@ public void createTechnicalMetadata(String aipId, String representationId, Strin } } + @Override + public void updateTechnicalMetadata(String aipId, String representationId, String metadataType, String fileId, + ContentPayload payload, String createdBy, boolean notify) throws AuthorizationDeniedException, + RequestNotValidException, AlreadyExistsException, NotFoundException, GenericException { + List operationLogs = operationRegistry.registerOperationForRepresentation(aipId, + representationId, OperationType.UPDATE); + try { + getModelService().updateTechnicalMetadata(aipId, representationId, metadataType, fileId, payload, createdBy, + notify); + operationRegistry.updateOperationState(operationLogs, OperationState.SUCCESS); + } catch (AuthorizationDeniedException | RequestNotValidException | AlreadyExistsException | NotFoundException + | GenericException e) { + operationRegistry.updateOperationState(operationLogs, OperationState.FAILURE); + throw e; + } + } + @Override public PreservationMetadata createPreservationMetadata(PreservationMetadata.PreservationMetadataType type, String aipId, List fileDirectoryPath, String fileId, ContentPayload payload, String username, diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/LiteRODAObjectFactory.java b/roda-core/roda-core/src/main/java/org/roda/core/model/LiteRODAObjectFactory.java index 7b3adab350..906fe14ea8 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/model/LiteRODAObjectFactory.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/model/LiteRODAObjectFactory.java @@ -42,6 +42,7 @@ import org.roda.core.data.v2.ip.metadata.OtherMetadata; import org.roda.core.data.v2.ip.metadata.PreservationMetadata; import org.roda.core.data.v2.ip.metadata.PreservationMetadata.PreservationMetadataType; +import org.roda.core.data.v2.ip.metadata.TechnicalMetadata; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.log.LogEntry; @@ -162,6 +163,8 @@ public static Optional get(T object) { ret = getOtherMetadata(object); } else if (object instanceof PreservationMetadata) { ret = getPreservationMetadata(object); + } else if (object instanceof TechnicalMetadata) { + ret = getTechnicalMetadata(object); } else if (object instanceof IndexedPreservationEvent) { ret = getIndexedPreservationEvent(object); } else if (object instanceof DIPFile) { @@ -215,6 +218,10 @@ public static Optional get(Class obj if (ids.size() == 2 || ids.size() == 3) { ret = create(objectClass, ids.size(), ids); } + } else if (objectClass == TechnicalMetadata.class) { + if (ids.size() >= 3) { + ret = create(objectClass, ids.size(), ids); + } } else if (objectClass == OtherMetadata.class) { if (ids.size() == 2 || ids.size() == 3) { ret = create(objectClass, ids.size(), ids); @@ -265,6 +272,16 @@ private static Optional getDescriptiveM return ret; } + private static Optional getTechnicalMetadata(T object) { + Optional ret; + TechnicalMetadata o = (TechnicalMetadata) object; + List list = new ArrayList<>(); + list.add(o.getAipId()); + list.add(o.getRepresentationId()); + list.add(o.getId()); + return get(TechnicalMetadata.class, list, false); + } + private static Optional getOtherMetadata(T object) { Optional ret; diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelService.java b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelService.java index f488a91b51..1ef761900e 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelService.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelService.java @@ -164,6 +164,9 @@ Binary retrieveDescriptiveMetadataBinary(String aipId, String descriptiveMetadat Binary retrieveDescriptiveMetadataBinary(String aipId, String representationId, String descriptiveMetadataId) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException; + Binary retrieveTechnicalMetadataBinary(String aipId, String representationId, List fileDirectoryPath, String fileId) + throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException ; + DescriptiveMetadata retrieveDescriptiveMetadata(String aipId, String descriptiveMetadataId) throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException; @@ -381,6 +384,10 @@ PreservationMetadata createPreservationMetadata(PreservationMetadata.Preservatio void createTechnicalMetadata(String aipId, String representationId, String metadataType, String fileId, ContentPayload payload, String createdBy, boolean notify) throws AuthorizationDeniedException, RequestNotValidException, AlreadyExistsException, NotFoundException, GenericException; + + void updateTechnicalMetadata(String aipId, String representationId, String metadataType, String fileId, + ContentPayload payload, String createdBy, boolean notify) throws AuthorizationDeniedException, + RequestNotValidException, AlreadyExistsException, NotFoundException, GenericException; public PreservationMetadata createPreservationMetadata(PreservationMetadataType type, String aipId, List fileDirectoryPath, String fileId, ContentPayload payload, String username, boolean notify) diff --git a/roda-core/roda-core/src/main/java/org/roda/core/transaction/TransactionalModelOperationRegistry.java b/roda-core/roda-core/src/main/java/org/roda/core/transaction/TransactionalModelOperationRegistry.java index 7981a59832..5fdc249b8d 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/transaction/TransactionalModelOperationRegistry.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/transaction/TransactionalModelOperationRegistry.java @@ -33,6 +33,7 @@ import org.roda.core.data.v2.ip.TransferredResource; import org.roda.core.data.v2.ip.metadata.DescriptiveMetadata; import org.roda.core.data.v2.ip.metadata.PreservationMetadata; +import org.roda.core.data.v2.ip.metadata.TechnicalMetadata; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.log.LogEntry; @@ -113,6 +114,21 @@ public List registerOperationForDescriptiveMetad return operationLogs; } + public List registerOperationForTechnicalMetadata(String aipID, + String representationId, List fileDirectoryPath, String fileId, OperationType operation) { + List operationLogs = new ArrayList<>(); + operationLogs.add(registerOperationForRelatedAIP(aipID, operation)); + List list = new ArrayList<>(); + list.add(aipID); + list.addAll(fileDirectoryPath); + if (representationId != null) { + list.add(representationId); + } + list.add(fileId); + operationLogs.add(registerOperation(TechnicalMetadata.class, list, operation)); + return operationLogs; + } + public List registerOperationForRepresentation(String aipID, String representationId, OperationType operation) { List operationLogs = new ArrayList<>(); @@ -511,7 +527,6 @@ public void releaseLock(Class objectClass, String id "[transactionId:" + transaction.getId() + "] Object class is not lockable: " + objectClass.getName()); } - Optional liteRODAObject = LiteRODAObjectFactory.get(objectClass, id); if (liteRODAObject.isPresent()) { String lite = liteRODAObject.get().getInfo(); diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java index 502f8c3188..0953865e61 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java @@ -392,18 +392,18 @@ public StreamResponse retrieveFileTechnicalMetadataHTML(RequestContext requestCo NotFoundException, GenericException, TechnicalMetadataNotFoundException { ModelService model = requestContext.getModelService(); Representation representation = model.retrieveRepresentation(file.getAipId(), file.getRepresentationId()); - String techMDURN = URNUtils.createRodaTechnicalMetadataURN(file.getId(), + String techMDURN = URNUtils.createRodaTechnicalMetadataURN(IdUtils.createTechnicalMetadataFileId(file.getId(), file.getPath()), RODAInstanceUtils.getLocalInstanceIdentifier(), type.toLowerCase()); Binary metadataBinary; if (versionID != null) { BinaryVersion binaryVersion = model.getBinaryVersion(representation, versionID, List.of(RodaConstants.STORAGE_DIRECTORY_METADATA, RodaConstants.STORAGE_DIRECTORY_TECHNICAL, type, - techMDURN + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION)); + techMDURN)); metadataBinary = binaryVersion.getBinary(); } else { metadataBinary = model.getBinary(representation, RodaConstants.STORAGE_DIRECTORY_METADATA, RodaConstants.STORAGE_DIRECTORY_TECHNICAL, type, - techMDURN + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION); + techMDURN); } String filename = metadataBinary.getStoragePath().getName() + HTML_EXT; String htmlDescriptive = HTMLUtils.technicalMetadataToHtml(metadataBinary, type, versionID, @@ -427,18 +427,18 @@ public StreamResponse retrieveFileTechnicalMetadata(RequestContext requestContex StreamResponse ret; ModelService model = requestContext.getModelService(); Representation representation = model.retrieveRepresentation(file.getAipId(), file.getRepresentationId()); - String techMDURN = URNUtils.createRodaTechnicalMetadataURN(file.getId(), + String techMDURN = URNUtils.createRodaTechnicalMetadataURN(IdUtils.createTechnicalMetadataFileId(file.getId(), file.getPath()), RODAInstanceUtils.getLocalInstanceIdentifier(), type.toLowerCase()); Binary metadataBinary; if (versionID != null) { BinaryVersion binaryVersion = model.getBinaryVersion(representation, versionID, List.of(RodaConstants.STORAGE_DIRECTORY_METADATA, RodaConstants.STORAGE_DIRECTORY_TECHNICAL, type, - techMDURN + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION)); + techMDURN)); metadataBinary = binaryVersion.getBinary(); } else { metadataBinary = model.getBinary(representation, RodaConstants.STORAGE_DIRECTORY_METADATA, RodaConstants.STORAGE_DIRECTORY_TECHNICAL, type, - techMDURN + RodaConstants.REPRESENTATION_INFORMATION_FILE_EXTENSION); + techMDURN); } stream = new BinaryConsumesOutputStream(metadataBinary, RodaConstants.MEDIA_TYPE_TEXT_XML); From 6f9b91d53db9a0100b4dfbcccd3c723978828698 Mon Sep 17 00:00:00 2001 From: Luis Faria Date: Tue, 9 Dec 2025 12:31:48 +0000 Subject: [PATCH 14/22] [Snyk] Upgrade org.apache.zookeeper:zookeeper from 3.9.3 to 3.9.4 (#3544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update release workflow * fix: upgrade org.apache.zookeeper:zookeeper from 3.9.3 to 3.9.4 Snyk has created this PR to upgrade org.apache.zookeeper:zookeeper from 3.9.3 to 3.9.4. See this package in maven: org.apache.zookeeper:zookeeper See this project in Snyk: https://app.snyk.io/org/luis100/project/d4f34799-92b4-4ac4-9760-afd88ae499a6?utm_source=github&utm_medium=referral&page=upgrade-pr --------- Co-authored-by: Miguel Guimarães Co-authored-by: snyk-bot --- roda-core/roda-core/pom.xml | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/roda-core/roda-core/pom.xml b/roda-core/roda-core/pom.xml index d3897a54d2..78b96d1278 100644 --- a/roda-core/roda-core/pom.xml +++ b/roda-core/roda-core/pom.xml @@ -46,16 +46,12 @@ jar-with-dependencies false - - - + + + org.roda.core.RodaCoreFactory - + reference.conf @@ -96,16 +92,12 @@ jar-with-dependencies false - - - + + + org.roda.core.RodaCoreFactory - + reference.conf @@ -164,7 +156,7 @@ org.apache.zookeeper zookeeper - 3.9.3 + 3.9.4 From aa2adcf0a7040770667431c12430eaeeb0ff2d7b Mon Sep 17 00:00:00 2001 From: Luis Faria Date: Tue, 9 Dec 2025 12:33:46 +0000 Subject: [PATCH 15/22] [Snyk] Upgrade commons-io:commons-io from 2.19.0 to 2.20.0 (#3541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update release workflow * fix: upgrade commons-io:commons-io from 2.19.0 to 2.20.0 Snyk has created this PR to upgrade commons-io:commons-io from 2.19.0 to 2.20.0. See this package in maven: commons-io:commons-io See this project in Snyk: https://app.snyk.io/org/luis100/project/b1187a31-133a-4d18-baa0-020a0f39b9ce?utm_source=github&utm_medium=referral&page=upgrade-pr --------- Co-authored-by: Miguel Guimarães Co-authored-by: snyk-bot --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fd06d1da61..a0f3ffc3cc 100644 --- a/pom.xml +++ b/pom.xml @@ -560,7 +560,7 @@ commons-io commons-io - 2.19.0 + 2.20.0 commons-logging From 2c13411f511fd991f89a7c07217f7a46ee7a58a7 Mon Sep 17 00:00:00 2001 From: Luis Faria Date: Tue, 9 Dec 2025 12:34:28 +0000 Subject: [PATCH 16/22] [Snyk] Upgrade com.fasterxml.jackson.core:jackson-core from 2.19.1 to 2.20.0 (#3540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update release workflow * fix: upgrade com.fasterxml.jackson.core:jackson-core from 2.19.1 to 2.20.0 Snyk has created this PR to upgrade com.fasterxml.jackson.core:jackson-core from 2.19.1 to 2.20.0. See this package in maven: com.fasterxml.jackson.core:jackson-core See this project in Snyk: https://app.snyk.io/org/luis100/project/d3ada05e-051e-43be-8c3f-b5ae9e54fc0f?utm_source=github&utm_medium=referral&page=upgrade-pr --------- Co-authored-by: Miguel Guimarães Co-authored-by: snyk-bot --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a0f3ffc3cc..e5902bedc1 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ 2.12.2 4.0.4 2.2.34 - 2.19.1 + 2.20.0 6.2.11 9.8.1 1.1.4 From 988a6891839c781e9234793eee7c3b8bf0f213ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Tue, 9 Dec 2025 12:47:11 +0000 Subject: [PATCH 17/22] Apply code-style rules --- .../main/java/org/roda/core/storage/fs/FileStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java index 02226c0103..455287745f 100644 --- a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java +++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FileStorageService.java @@ -568,7 +568,7 @@ public void copy(StorageService fromService, StoragePath fromStoragePath, Storag @Override public void export(StorageService fromService, StoragePath fromStoragePath, Path toPath, String resource, - boolean replaceExisting) throws AlreadyExistsException, GenericException { + boolean replaceExisting) throws AlreadyExistsException, GenericException { Path sourcePath = null; if (StringUtils.isNotBlank(resource)) { sourcePath = FSUtils.getEntityPath(basePath, fromStoragePath).resolve(resource); From 45abe94b1d6b4839fa7632dad982e289294d6ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Tue, 9 Dec 2025 13:32:02 +0000 Subject: [PATCH 18/22] Use jackson-bom --- pom.xml | 47 ++++++++--------------------------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/pom.xml b/pom.xml index e5902bedc1..0ff82a3797 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ 2.12.2 4.0.4 2.2.34 - 2.20.0 + 2.20.1 6.2.11 9.8.1 1.1.4 @@ -478,6 +478,13 @@ jaxb-runtime 4.0.5 + + com.fasterxml.jackson + jackson-bom + 2.20.0 + import + pom + org.springframework.boot @@ -486,36 +493,6 @@ pom import - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - org.gwtproject gwt-dev @@ -960,19 +937,11 @@ icu4j 63.2 - org.springframework.boot spring-boot-starter-tomcat provided - - - - com.google.code.gson - gson - 2.11.0 - From 4020b7db17bb9429e56c3914413583259b7a04ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Tue, 9 Dec 2025 14:01:29 +0000 Subject: [PATCH 19/22] Security upgrade org.testcontainers:testcontainers from 1.21.1 to 2.0.2 --- roda-core/roda-core-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roda-core/roda-core-tests/pom.xml b/roda-core/roda-core-tests/pom.xml index 1373dcfc8e..011b376fea 100644 --- a/roda-core/roda-core-tests/pom.xml +++ b/roda-core/roda-core-tests/pom.xml @@ -155,7 +155,7 @@ org.testcontainers testcontainers - 1.21.1 + 2.0.2 From e19abc3bcc6eb7beaf045206a0483afe73c97d89 Mon Sep 17 00:00:00 2001 From: Alexandre Flores <40147374+SugaryLump@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:24:15 +0000 Subject: [PATCH 20/22] Re add getOtherMetadata API method (#3538) * WIP readd retrieveOtherMetadata API method * Finish readding getOtherMetadata API method * moved file API method from interface to controller --- .../config/roda-permissions.properties | 1 + .../resources/config/roda-roles.properties | 1 + .../api/v2/controller/FilesController.java | 32 +++++++++++++++++-- .../wui/api/v2/services/FilesService.java | 11 +++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/roda-core/roda-core/src/main/resources/config/roda-permissions.properties b/roda-core/roda-core/src/main/resources/config/roda-permissions.properties index 826a491ea7..29a39e09fc 100644 --- a/roda-core/roda-core/src/main/resources/config/roda-permissions.properties +++ b/roda-core/roda-core/src/main/resources/config/roda-permissions.properties @@ -88,6 +88,7 @@ core.permissions.org.roda.wui.api.v2.controller.FilesController.retrievePreserva core.permissions.org.roda.wui.api.v2.controller.FilesController.retrieveTechnicalMetadataInfos=READ core.permissions.org.roda.wui.api.v2.controller.FilesController.retrieveFileTechnicalMetadataHTML=READ core.permissions.org.roda.wui.api.v2.controller.FilesController.retrieveTechnicalMetadataFile=READ +core.permissions.org.roda.wui.api.v2.controller.FilesController.getOtherMetadata = READ # Preservation events permissions core.permissions.org.roda.wui.api.v2.controller.PreservationEventController.downloadPreservationEvent = READ diff --git a/roda-core/roda-core/src/main/resources/config/roda-roles.properties b/roda-core/roda-core/src/main/resources/config/roda-roles.properties index 0cb9043586..33564cc43e 100644 --- a/roda-core/roda-core/src/main/resources/config/roda-roles.properties +++ b/roda-core/roda-core/src/main/resources/config/roda-roles.properties @@ -130,6 +130,7 @@ core.roles.org.roda.wui.api.v2.controller.FilesController.retrievePreservationMe core.roles.org.roda.wui.api.v2.controller.FilesController.retrieveTechnicalMetadataInfos=preservation_metadata.read core.roles.org.roda.wui.api.v2.controller.FilesController.retrieveFileTechnicalMetadataHTML=preservation_metadata.read core.roles.org.roda.wui.api.v2.controller.FilesController.retrieveTechnicalMetadataFile=preservation_metadata.read +core.roles.org.roda.wui.api.v2.controller.FilesController.getOtherMetadata = preservation_metadata.read # Metrics roles core.roles.org.roda.wui.api.v2.controller.MetricsController.getMetrics = job.manage diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/FilesController.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/FilesController.java index c019240193..aca6421f5b 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/FilesController.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/FilesController.java @@ -34,6 +34,7 @@ import org.roda.core.data.v2.ip.IndexedAIP; import org.roda.core.data.v2.ip.IndexedFile; import org.roda.core.data.v2.ip.IndexedRepresentation; +import org.roda.core.data.v2.ip.metadata.OtherMetadata; import org.roda.core.data.v2.ip.metadata.TechnicalMetadataInfos; import org.roda.core.data.v2.jobs.Job; import org.roda.core.model.utils.UserUtility; @@ -413,8 +414,7 @@ public List process(RequestContext requestContext, RequestControllerAssi @Override public ResponseEntity exportToCSV(String findRequestString) { // delegate - return ApiUtils - .okResponse(indexService.exportToCSV(findRequestString, IndexedFile.class)); + return ApiUtils.okResponse(indexService.exportToCSV(findRequestString, IndexedFile.class)); } @GetMapping(path = "/{uuid}/metadata/technical/{typeId}/html", produces = MediaType.TEXT_HTML_VALUE) @@ -483,4 +483,32 @@ public ResponseEntity process(RequestContext requestConte } }); } + + @RequestMapping(method = RequestMethod.GET, path = "/{fileUUID}/other_metadata/{metadata_type}/{metadata_file_suffix}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Gets other metadata (JSON info or ZIP file).\nOptional query params of **start** and **limit** defined the returned query", responses = { + @ApiResponse(responseCode = "200", description = "Other metadata file", content = @Content(schema = @Schema(implementation = OtherMetadata.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class))), + @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class))), + @ApiResponse(responseCode = "404", description = "Not found", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class)))}) + ResponseEntity getOtherMetadata( + @Parameter(description = "The UUID of the existing File", required = true) @PathVariable(name = "fileUUID") String fileUUID, + @Parameter(description = "The type of the other metadata", required = true) @PathVariable(name = "metadata_type") String metadataType, + @Parameter(description = "The file suffix of the other metadata", required = true) @PathVariable(name = "metadata_file_suffix") String metadataFileSuffix) { + return requestHandler.processRequest(new RequestHandler.RequestProcessor>() { + @Override + public ResponseEntity process(RequestContext requestContext, + RequestControllerAssistant controllerAssistant) throws RODAException, RESTException, IOException { + controllerAssistant.setParameters(RodaConstants.CONTROLLER_FILE_ID_PARAM, fileUUID); + // check object permissions + IndexedFile indexedFile = requestContext.getIndexService().retrieve(IndexedFile.class, fileUUID, + RodaConstants.FILE_FIELDS_TO_RETURN); + controllerAssistant.checkObjectPermissions(requestContext.getUser(), indexedFile); + + // delegate + StreamResponse streamResponse = filesService.retrieveOtherMetadata(requestContext, indexedFile, metadataType, + metadataFileSuffix); + return ApiUtils.okResponse(streamResponse); + } + }); + } } diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java index 0953865e61..7a808073ed 100644 --- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java +++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/FilesService.java @@ -447,4 +447,15 @@ public StreamResponse retrieveFileTechnicalMetadata(RequestContext requestContex return ret; } + public StreamResponse retrieveOtherMetadata(RequestContext requestContext, IndexedFile file, String metadataType, + String metadataSuffix) + throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException { + final ConsumesOutputStream stream; + ModelService model = requestContext.getModelService(); + Binary otherMetadataBinary = model.retrieveOtherMetadataBinary(file.getAipId(), file.getRepresentationId(), + file.getPath(), file.getId(), metadataSuffix, metadataType); + stream = new BinaryConsumesOutputStream(otherMetadataBinary, RodaConstants.MEDIA_TYPE_TEXT_HTML); + return new StreamResponse(stream); + } + } From 453e8150b22aad62d654f7f9e38f03afecc7b67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Guimar=C3=A3es?= Date: Tue, 9 Dec 2025 14:30:19 +0000 Subject: [PATCH 21/22] Security upgrade org.apache.solr from 9.8.1 to 9.10.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0ff82a3797..4fab96416e 100644 --- a/pom.xml +++ b/pom.xml @@ -125,7 +125,7 @@ 2.2.34 2.20.1 6.2.11 - 9.8.1 + 9.10.0 1.1.4 5.5 2.10.0 From 389aee93c53fdcada0e476edab9c73ead64eebf5 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 10 Dec 2025 12:04:00 +0000 Subject: [PATCH 22/22] fix: upgrade org.springframework.boot:spring-boot-starter from 3.4.10 to 3.5.7 Snyk has created this PR to upgrade org.springframework.boot:spring-boot-starter from 3.4.10 to 3.5.7. See this package in maven: org.springframework.boot:spring-boot-starter See this project in Snyk: https://app.snyk.io/org/luis100/project/4b5443af-2212-4a11-9910-1ef16f10c5ac?utm_source=github&utm_medium=referral&page=upgrade-pr --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4fab96416e..b63ce7768c 100644 --- a/pom.xml +++ b/pom.xml @@ -132,7 +132,7 @@ 3.2.6 https://roda-community.org all - 3.4.10 + 3.5.7 provided 1.5.19