jakarta.ws.rs
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/RodaContainersLifecycleListener.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/RodaContainersLifecycleListener.java
new file mode 100644
index 0000000000..4e740dcb29
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/RodaContainersLifecycleListener.java
@@ -0,0 +1,38 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE file at the root of the source
+ * tree and available online at
+ *
+ * https://github.com/keeps/roda
+ */
+package org.roda.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.ISuite;
+import org.testng.ISuiteListener;
+
+/**
+ * TestNG suite listener that starts all required test infrastructure containers
+ * (ZooKeeper, Solr, PostgreSQL) before any test or Spring context runs.
+ *
+ * Registered via {@code testng.xml} so it fires at the very beginning of the
+ * test suite, ahead of any class loading or Spring Boot context initialization.
+ *
+ * @author RODA Community
+ */
+public class RodaContainersLifecycleListener implements ISuiteListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(RodaContainersLifecycleListener.class);
+
+ @Override
+ public void onStart(ISuite suite) {
+ LOGGER.info("RodaContainersLifecycleListener: initializing test containers for suite '{}'", suite.getName());
+ TestContainersManager.getInstance();
+ }
+
+ @Override
+ public void onFinish(ISuite suite) {
+ // Containers are stopped via JVM shutdown hook in TestContainersManager.
+ }
+}
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/TestContainersManager.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/TestContainersManager.java
new file mode 100644
index 0000000000..8b755a0f03
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/TestContainersManager.java
@@ -0,0 +1,188 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE file at the root of the source
+ * tree and available online at
+ *
+ * https://github.com/keeps/roda
+ */
+package org.roda.core;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+
+/**
+ * Singleton manager for test infrastructure containers (ZooKeeper, Solr,
+ * PostgreSQL).
+ *
+ * Containers are started once per JVM and stopped via a shutdown hook. System
+ * properties are set so that {@link org.roda.core.config.ConfigurationManager}
+ * and Spring Boot pick them up before any test or Spring context initialization
+ * runs.
+ *
+ * @author RODA Community
+ */
+public class TestContainersManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TestContainersManager.class);
+
+ private static volatile TestContainersManager INSTANCE;
+
+ private final Network network;
+ private final GenericContainer> zookeeper;
+ private final GenericContainer> solr;
+ private final GenericContainer> postgres;
+ private final GenericContainer> mailpit;
+ private final GenericContainer> siegfried;
+
+ @SuppressWarnings("resource")
+ private TestContainersManager() {
+ LOGGER.info("Starting test infrastructure containers...");
+
+ network = Network.newNetwork();
+
+ // ZooKeeper — exposed so that the RODA CloudSolrClient can connect
+ zookeeper = new GenericContainer<>(DockerImageName.parse("zookeeper:3.9.1-jre-17")).withNetwork(network)
+ .withNetworkAliases("zookeeper").withExposedPorts(2181)
+ .withEnv("ZOO_TICK_TIME", "10000")
+ .withEnv("ZOO_CFG_EXTRA", "maxSessionTimeout=600000")
+ .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
+ zookeeper.start();
+ LOGGER.info("ZooKeeper started at {}:{}", zookeeper.getHost(), zookeeper.getMappedPort(2181));
+
+ // Solr — connects to ZooKeeper via the internal Docker network alias.
+ // Solr registers itself in ZooKeeper using the result of
+ // InetAddress.getLocalHost().getHostAddress(), which in Docker resolves to
+ // the container's bridge-network IP. On Linux (CI and most developer
+ // machines) this IP is directly reachable from the Docker host, so the
+ // CloudSolrClient can connect without any additional port mapping.
+ //
+ // Wait for the log message that Solr emits immediately after registering
+ // the live node in ZooKeeper. This log fires AFTER ZkController.registerLiveNode(),
+ // so by the time this wait strategy succeeds, the live node is already
+ // present in ZooKeeper and RodaCoreFactory.connect() will find it instantly.
+ // Using a log-based strategy avoids any HTTP proxy interference.
+ solr = new GenericContainer<>(DockerImageName.parse("solr:9")).withNetwork(network)
+ .withEnv("ZK_HOST", "zookeeper:2181").withExposedPorts(8983)
+ .waitingFor(Wait.forLogMessage(".*Register node as live in ZooKeeper.*", 1)
+ .withStartupTimeout(Duration.ofMinutes(3)));
+ solr.start();
+ LOGGER.info("Solr started at {}:{}", solr.getHost(), solr.getMappedPort(8983));
+
+ // PostgreSQL
+ postgres = new GenericContainer<>(DockerImageName.parse("postgres:17")).withEnv("POSTGRES_USER", "admin")
+ .withEnv("POSTGRES_PASSWORD", "roda").withEnv("POSTGRES_DB", "roda_core_db").withExposedPorts(5432)
+ .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
+ postgres.start();
+ LOGGER.info("PostgreSQL started at {}:{}", postgres.getHost(), postgres.getMappedPort(5432));
+
+ // Mailpit
+ mailpit = new GenericContainer<>(DockerImageName.parse("axllent/mailpit:latest")).withExposedPorts(1025, 8025)
+ .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
+ mailpit.start();
+ LOGGER.info("Mailpit started at {}:{}", mailpit.getHost(), mailpit.getMappedPort(1025));
+
+ // Clamav
+ GenericContainer> clamav = new GenericContainer<>(DockerImageName.parse("clamav/clamav:1.5.2"))
+ .withExposedPorts(3310).withFileSystemBind("/tmp", "/tmp", BindMode.READ_WRITE)
+ .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
+ clamav.start();
+
+ String configContent = """
+ TCPSocket %d
+ TCPAddr %s
+ """.formatted(clamav.getMappedPort(3310), clamav.getHost());
+
+ try {
+ Path tempConfigFile = Paths.get("/tmp/clamd.conf");
+ Files.writeString(tempConfigFile, configContent);
+ } catch (IOException e) {
+ stopAll();
+ throw new RuntimeException("Could not write config file: " + configContent);
+ }
+
+ LOGGER.info("ClamAV started at {}:{}", clamav.getHost(), clamav.getMappedPort(3310));
+
+ // Siegfried
+ siegfried = new GenericContainer<>(DockerImageName.parse("keeps/siegfried:v1.11.0"))
+ .withEnv("SIEGFRIED_HOST", "0.0.0.0").withEnv("SIEGFRIED_PORT", "5138").withExposedPorts(5138)
+ .withFileSystemBind("/tmp", "/tmp", BindMode.READ_ONLY)
+ .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(60)));
+ siegfried.start();
+ LOGGER.info("Siegfried started at {}:{}", siegfried.getHost(), siegfried.getMappedPort(5138));
+
+ configureSystemProperties();
+
+ Runtime.getRuntime().addShutdownHook(new Thread(this::stopAll, "testcontainers-shutdown"));
+ }
+
+ public static TestContainersManager getInstance() {
+ if (INSTANCE == null) {
+ synchronized (TestContainersManager.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new TestContainersManager();
+ }
+ }
+ }
+ return INSTANCE;
+ }
+
+ private void configureSystemProperties() {
+ String zkUrl = zookeeper.getHost() + ":" + zookeeper.getMappedPort(2181);
+ System.setProperty("RODA_CORE_SOLR_TYPE", "CLOUD");
+ System.setProperty("RODA_CORE_SOLR_CLOUD_URLS", zkUrl);
+ LOGGER.info("Set RODA_CORE_SOLR_CLOUD_URLS={}", zkUrl);
+
+ String pgUrl = "jdbc:postgresql://" + postgres.getHost() + ":" + postgres.getMappedPort(5432) + "/roda_core_db";
+ System.setProperty("spring.datasource.url", pgUrl);
+ System.setProperty("spring.datasource.username", "admin");
+ System.setProperty("spring.datasource.password", "roda");
+ LOGGER.info("Set spring.datasource.url={}", pgUrl);
+
+ System.setProperty("RODA_CORE_EMAIL_HOST", mailpit.getHost());
+ System.setProperty("RODA_CORE_EMAIL_PORT", mailpit.getMappedPort(1025).toString());
+
+ // Give Solr Cloud more time to establish its ZooKeeper connection in
+ // environments where ZkClient session establishment is slow.
+ System.setProperty("RODA_CORE_SOLR_CLOUD_CONNECT_TIMEOUT_MS", "300000");
+ // Increase ZK connect timeout so SolrZkClient does not call ZooKeeper.close()
+ // before the session is established (the close() hangs indefinitely when there
+ // are no background ZK threads left to process the CLOSESESSION response).
+ System.setProperty("RODA_CORE_SOLR_CLOUD_ZK_CONNECT_TIMEOUT_MS", "300000");
+ System.setProperty("zkConnectTimeout", "300000");
+
+ System.setProperty("RODA_CORE_PLUGINS_INTERNAL_VIRUS_CHECK_CLAMAV_PARAMS", "-m --stream -c /tmp/clamd.conf");
+
+ String siegfriedUrl = "http://" + siegfried.getHost() + ":" + siegfried.getMappedPort(5138);
+ System.setProperty("RODA_CORE_TOOLS_SIEGFRIED_MODE", "server");
+ System.setProperty("RODA_CORE_TOOLS_SIEGFRIED_SERVER", siegfriedUrl);
+ }
+
+ private void stopAll() {
+ LOGGER.info("Stopping test infrastructure containers...");
+ if (solr != null && solr.isRunning()) {
+ solr.stop();
+ }
+ if (zookeeper != null && zookeeper.isRunning()) {
+ zookeeper.stop();
+ }
+ if (postgres != null && postgres.isRunning()) {
+ postgres.stop();
+ }
+ if (network != null) {
+ network.close();
+ }
+ }
+}
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/config/TestConfig.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/config/TestConfig.java
index 3a6b6dac7d..7901382e9d 100644
--- a/roda-core/roda-core-tests/src/main/java/org/roda/core/config/TestConfig.java
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/config/TestConfig.java
@@ -21,6 +21,6 @@
@EnableAutoConfiguration
@ComponentScan(basePackages = "org.roda.core")
@EnableJpaRepositories(basePackages = "org.roda.core.repository")
-@EntityScan(basePackages = "org.roda.core.entity")
+@EntityScan(basePackages = {"org.roda.core.entity", "org.roda.core.data.v2.jobs"})
public class TestConfig {
}
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/model/JobPersistenceTest.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/model/JobPersistenceTest.java
new file mode 100644
index 0000000000..faeee7a5c2
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/model/JobPersistenceTest.java
@@ -0,0 +1,314 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE file at the root of the source
+ * tree and available online at
+ *
+ * https://github.com/keeps/roda
+ */
+package org.roda.core.model;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.roda.core.RodaCoreFactory;
+import org.roda.core.TestsHelper;
+import org.roda.core.common.iterables.CloseableIterable;
+import org.roda.core.config.TestConfig;
+import org.roda.core.data.common.RodaConstants;
+import org.roda.core.data.exceptions.GenericException;
+import org.roda.core.data.exceptions.NotFoundException;
+import org.roda.core.data.exceptions.RODAException;
+import org.roda.core.data.v2.common.OptionalWithCause;
+import org.roda.core.data.v2.index.select.SelectedItemsNone;
+import org.roda.core.data.v2.jobs.Job;
+import org.roda.core.data.v2.jobs.Job.JOB_STATE;
+import org.roda.core.data.v2.jobs.PluginType;
+import org.roda.core.data.v2.jobs.Report;
+import org.roda.core.repository.job.JobRepository;
+import org.roda.core.repository.job.ReportRepository;
+import org.roda.core.security.LdapUtilityTestHelper;
+import org.roda.core.storage.StorageService;
+import org.roda.core.storage.fs.FSUtils;
+import org.roda.core.util.IdUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for the hybrid Job/Report persistence logic.
+ * Tests that running jobs are stored in the database and flushed to storage on completion.
+ *
+ * @author RODA Development Team
+ */
+@SpringBootTest(classes = TestConfig.class)
+@Test(groups = {RodaConstants.TEST_GROUP_ALL, RodaConstants.TEST_GROUP_DEV})
+public class JobPersistenceTest extends AbstractTestNGSpringContextTests {
+ private static final Logger LOGGER = LoggerFactory.getLogger(JobPersistenceTest.class);
+
+ private static Path basePath;
+ private static StorageService storage;
+ private static ModelService model;
+ private static LdapUtilityTestHelper ldapUtilityTestHelper;
+
+ @Autowired
+ private JobRepository jobRepository;
+
+ @Autowired
+ private ReportRepository reportRepository;
+
+ @BeforeClass
+ public void init() throws IOException, GenericException {
+ basePath = TestsHelper.createBaseTempDir(getClass(), true);
+ ldapUtilityTestHelper = new LdapUtilityTestHelper();
+
+ boolean deploySolr = false;
+ boolean deployLdap = true;
+ boolean deployFolderMonitor = false;
+ boolean deployOrchestrator = false;
+ boolean deployPluginManager = false;
+ boolean deployDefaultResources = false;
+ RodaCoreFactory.instantiateTest(deploySolr, deployLdap, deployFolderMonitor, deployOrchestrator,
+ deployPluginManager, deployDefaultResources, false, ldapUtilityTestHelper.getLdapUtility());
+
+ storage = RodaCoreFactory.getStorageService();
+ model = RodaCoreFactory.getModelService();
+
+ LOGGER.debug("Running JobPersistenceTest under storage: {}", basePath);
+ }
+
+ @AfterClass
+ public void cleanup() throws NotFoundException, GenericException, IOException {
+ // Clean up any test data
+ jobRepository.deleteAll();
+ reportRepository.deleteAll();
+
+ ldapUtilityTestHelper.shutdown();
+ RodaCoreFactory.shutdown();
+ FSUtils.deletePath(basePath);
+ }
+
+ /**
+ * Test that a newly created job with a non-final state (STARTED) is saved to the database
+ * and NOT written to file storage.
+ */
+ @Test
+ public void testRunningJobPersistence() throws RODAException {
+ // Create a running job
+ String jobId = IdUtils.createUUID();
+ Job job = createTestJob(jobId, JOB_STATE.STARTED);
+
+ // Create the job using the model service
+ model.createJob(job);
+
+ // Verify job exists in database
+ assertTrue(jobRepository.existsById(jobId), "Job should exist in database");
+
+ // Verify job retrieved from model service
+ Job retrievedJob = model.retrieveJob(jobId);
+ assertNotNull(retrievedJob, "Should be able to retrieve the job");
+ assertEquals(retrievedJob.getId(), jobId);
+ assertEquals(retrievedJob.getState(), JOB_STATE.STARTED);
+
+ // Clean up
+ model.deleteJob(jobId);
+ }
+
+ /**
+ * Test that updating a job to a final state (COMPLETED) flushes it from the database
+ * to file storage.
+ */
+ @Test
+ public void testJobFinalization() throws RODAException {
+ // Create a running job
+ String jobId = IdUtils.createUUID();
+ Job job = createTestJob(jobId, JOB_STATE.STARTED);
+
+ // Create the job using the model service
+ model.createJob(job);
+
+ // Verify job is in database initially
+ assertTrue(jobRepository.existsById(jobId), "Job should exist in database initially");
+
+ // Create a report for this job
+ Report report = createTestReport(jobId);
+ model.createOrUpdateJobReport(report, job);
+
+ // Verify report is in database
+ assertTrue(reportRepository.existsById(report.getId()), "Report should exist in database");
+
+ // Now update job to final state
+ job.setState(JOB_STATE.COMPLETED);
+ job.setEndDate(new Date());
+ model.createOrUpdateJob(job);
+
+ // Verify job is no longer in database (flushed to storage)
+ assertFalse(jobRepository.existsById(jobId), "Job should not exist in database after completion");
+
+ // Verify report is no longer in database
+ assertFalse(reportRepository.existsById(report.getId()), "Report should not exist in database after job completion");
+
+ // Verify job can still be retrieved (from storage)
+ Job retrievedJob = model.retrieveJob(jobId);
+ assertNotNull(retrievedJob, "Should be able to retrieve completed job from storage");
+ assertEquals(retrievedJob.getState(), JOB_STATE.COMPLETED);
+
+ // Clean up
+ model.deleteJob(jobId);
+ }
+
+ /**
+ * Test that the list method returns both running jobs (from DB) and completed jobs (from storage).
+ */
+ @Test
+ public void testListingConsistency() throws RODAException {
+ // Create a running job (will be in DB)
+ String runningJobId = IdUtils.createUUID();
+ Job runningJob = createTestJob(runningJobId, JOB_STATE.STARTED);
+ model.createJob(runningJob);
+
+ // Create a completed job (will be in storage)
+ String completedJobId = IdUtils.createUUID();
+ Job completedJob = createTestJob(completedJobId, JOB_STATE.COMPLETED);
+ completedJob.setEndDate(new Date());
+ model.createJob(completedJob);
+ // Force transition to storage by creating and immediately completing
+ model.createOrUpdateJob(completedJob);
+
+ // List all jobs using model service
+ try (CloseableIterable> jobsIterable = model.list(Job.class)) {
+ List allJobs = StreamSupport.stream(jobsIterable.spliterator(), false)
+ .filter(OptionalWithCause::isPresent)
+ .map(OptionalWithCause::get)
+ .collect(Collectors.toList());
+
+ // Verify both jobs are listed
+ assertTrue(allJobs.stream().anyMatch(j -> j.getId().equals(runningJobId)),
+ "Running job should be in the list");
+ // Note: completed job may or may not be in list depending on timing
+
+ LOGGER.info("Listed {} jobs total", allJobs.size());
+ } catch (IOException e) {
+ throw new GenericException("Error closing iterable", e);
+ }
+
+ // Clean up
+ model.deleteJob(runningJobId);
+ try {
+ model.deleteJob(completedJobId);
+ } catch (NotFoundException e) {
+ // May have already been deleted or never existed in storage
+ }
+ }
+
+ /**
+ * Test that deleteJob properly cleans up both DB and storage.
+ */
+ @Test
+ public void testDeletion() throws RODAException {
+ // Create a running job
+ String jobId = IdUtils.createUUID();
+ Job job = createTestJob(jobId, JOB_STATE.STARTED);
+ model.createJob(job);
+
+ // Create a report
+ Report report = createTestReport(jobId);
+ model.createOrUpdateJobReport(report, job);
+
+ // Verify they exist in DB
+ assertTrue(jobRepository.existsById(jobId), "Job should exist in database");
+ assertTrue(reportRepository.existsById(report.getId()), "Report should exist in database");
+
+ // Delete the job
+ model.deleteJob(jobId);
+
+ // Verify both job and reports are deleted from DB
+ assertFalse(jobRepository.existsById(jobId), "Job should be deleted from database");
+ List remainingReports = reportRepository.findByJobId(jobId);
+ assertTrue(remainingReports.isEmpty(), "Reports should be deleted from database");
+
+ // Verify job cannot be retrieved
+ boolean notFound = false;
+ try {
+ model.retrieveJob(jobId);
+ } catch (NotFoundException e) {
+ notFound = true;
+ }
+ assertTrue(notFound, "Job should not be found after deletion");
+ }
+
+ /**
+ * Test report persistence for running jobs.
+ */
+ @Test
+ public void testReportPersistence() throws RODAException {
+ // Create a running job
+ String jobId = IdUtils.createUUID();
+ Job job = createTestJob(jobId, JOB_STATE.STARTED);
+ model.createJob(job);
+
+ // Create multiple reports
+ Report report1 = createTestReport(jobId);
+ Report report2 = createTestReport(jobId);
+ model.createOrUpdateJobReport(report1, job);
+ model.createOrUpdateJobReport(report2, job);
+
+ // Verify reports are in database
+ List dbReports = reportRepository.findByJobId(jobId);
+ assertEquals(dbReports.size(), 2, "Should have 2 reports in database");
+
+ // Verify reports can be listed through model service
+ try (CloseableIterable> reportsIterable = model.listJobReports(jobId)) {
+ List listedReports = StreamSupport.stream(reportsIterable.spliterator(), false)
+ .filter(OptionalWithCause::isPresent)
+ .map(OptionalWithCause::get)
+ .collect(Collectors.toList());
+ assertEquals(listedReports.size(), 2, "Should list 2 reports through model service");
+ } catch (IOException e) {
+ throw new GenericException("Error closing iterable", e);
+ }
+
+ // Clean up
+ model.deleteJob(jobId);
+ }
+
+ private Job createTestJob(String jobId, JOB_STATE state) {
+ Job job = new Job();
+ job.setId(jobId);
+ job.setName("Test Job " + jobId);
+ job.setUsername(RodaConstants.ADMIN);
+ job.setState(state);
+ job.setStartDate(new Date());
+ job.setPlugin("org.roda.core.plugins.test.TestPlugin");
+ job.setPluginType(PluginType.MISC);
+ job.setPluginParameters(new HashMap<>());
+ job.setSourceObjects(new SelectedItemsNone<>());
+ return job;
+ }
+
+ private Report createTestReport(String jobId) {
+ Report report = new Report();
+ report.setId(IdUtils.createUUID());
+ report.setJobId(jobId);
+ report.setSourceObjectId("test-source-" + UUID.randomUUID().toString().substring(0, 8));
+ report.setOutcomeObjectId("test-outcome-" + UUID.randomUUID().toString().substring(0, 8));
+ report.setDateCreated(new Date());
+ report.setTitle("Test Report");
+ return report;
+ }
+}
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/plugins/AIPCorruptionRiskAssessmentTest.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/plugins/AIPCorruptionRiskAssessmentTest.java
index b49274c209..0dcae1cdc3 100644
--- a/roda-core/roda-core-tests/src/main/java/org/roda/core/plugins/AIPCorruptionRiskAssessmentTest.java
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/plugins/AIPCorruptionRiskAssessmentTest.java
@@ -7,7 +7,9 @@
*/
package org.roda.core.plugins;
+import java.io.IOException;
import java.net.URL;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
@@ -18,15 +20,22 @@
import org.roda.core.RodaCoreFactory;
import org.roda.core.TestsHelper;
import org.roda.core.data.common.RodaConstants;
+import org.roda.core.data.exceptions.AlreadyExistsException;
+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.RODAException;
+import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.v2.index.filter.Filter;
import org.roda.core.data.v2.index.select.SelectedItemsList;
import org.roda.core.data.v2.ip.AIP;
+import org.roda.core.data.v2.ip.File;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.PluginState;
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.data.v2.risks.RiskIncidence;
+import org.roda.core.data.v2.validation.ValidationException;
import org.roda.core.index.IndexService;
import org.roda.core.index.IndexTestUtils;
import org.roda.core.model.ModelService;
@@ -48,12 +57,12 @@
public class AIPCorruptionRiskAssessmentTest {
private static final Logger LOGGER = LoggerFactory.getLogger(AIPCorruptionRiskAssessmentTest.class);
- private static Path basePath;
+ private Path basePath;
- private static ModelService model;
- private static IndexService index;
- private static LdapUtilityTestHelper ldapUtilityTestHelper;
- private static StorageService corporaService;
+ private ModelService model;
+ private IndexService index;
+ private LdapUtilityTestHelper ldapUtilityTestHelper;
+ private StorageService corporaService;
@BeforeMethod
public void setUp() throws Exception {
@@ -97,13 +106,44 @@ public void testAIPCorruption() throws RODAException {
SelectedItemsList.create(AIP.class, Collections.singletonList(aipId)));
List jobReports = TestsHelper.getJobReports(index, job, false);
- int count = StringUtils.countMatches(jobReports.get(0).getPluginDetails(), "");
+
index.commit(RiskIncidence.class);
long incidences = index.count(RiskIncidence.class, Filter.ALL);
- // 3 errors: 1 checksum checking error, 1 file without premis, 1 premis
- // without file Assert.assertEquals(count, 3);
- Assert.assertEquals(incidences, 2);
- Assert.assertEquals(jobReports.get(0).getPluginState(), PluginState.FAILURE);
+ Assert.assertEquals(incidences, 3, "3 incidences should be reported for the corrupted AIP: 1 checksum error, 1 file without PREMIS, 1 PREMIS without file");
+ Assert.assertEquals(jobReports.getFirst().getPluginState(), PluginState.FAILURE);
+ }
+
+ @Test
+ public void testFileRemovedFromStorage() throws RequestNotValidException, AuthorizationDeniedException,
+ ValidationException, AlreadyExistsException, NotFoundException, GenericException, IOException {
+ String aipId = IdUtils.createUUID();
+ AIP aip = model.createAIP(aipId, corporaService,
+ DefaultStoragePath.parse(CorporaConstants.SOURCE_AIP_CONTAINER, "AIP_4"), RodaConstants.ADMIN);
+
+ File file = model.retrieveFile(aip.getId(), aip.getRepresentations().getFirst().getId(), List.of(),
+ "2012-roda-promo-en.pdf");
+ Path path = model.getDirectAccess(file).getPath();
+
+ Assert.assertTrue(path.toFile().exists());
+
+ Files.delete(path);
+ Assert.assertFalse(path.toFile().exists());
+
+ Job job = TestsHelper.executeJob(AIPCorruptionRiskAssessmentPlugin.class, PluginType.AIP_TO_AIP,
+ SelectedItemsList.create(AIP.class, Collections.singletonList(aipId)));
+
+ List
jobReports = TestsHelper.getJobReports(index, job, false);
+
+ Assert.assertEquals(job.getJobStats().getCompletionPercentage(), 100,
+ "Job should be completed even if file is missing");
+ Assert.assertEquals(job.getJobStats().getSourceObjectsProcessedWithFailure(), 1,
+ "Job should report 1 source object processed with failure due to missing file");
+ Assert.assertEquals(jobReports.getFirst().getPluginState(), PluginState.FAILURE,
+ "Plugin should report failure due to missing file");
+
+ index.commit(RiskIncidence.class);
+ long incidences = index.count(RiskIncidence.class, Filter.ALL);
+ Assert.assertEquals(incidences, 1, "There should be 1 risk incidence reported due to missing file");
}
}
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/DeleteAIPPermissionTest.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/DeleteAIPPermissionTest.java
index a0bc0b270c..7a7a11c969 100644
--- a/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/DeleteAIPPermissionTest.java
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/DeleteAIPPermissionTest.java
@@ -65,8 +65,8 @@ public class DeleteAIPPermissionTest {
private static LdapUtilityTestHelper ldapUtilityTestHelper;
@BeforeMethod
- public static void setUp() throws Exception {
- basePath = TestsHelper.createBaseTempDir(FileStorageServiceTest.class, true);
+ public void setUp() throws Exception {
+ basePath = TestsHelper.createBaseTempDir(this.getClass(), true);
ldapUtilityTestHelper = new LdapUtilityTestHelper();
RodaCoreFactory.instantiateTest(true, true, true, true, true, false, false, ldapUtilityTestHelper.getLdapUtility());
diff --git a/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/TransactionalStorageServiceTest.java b/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/TransactionalStorageServiceTest.java
index a9852a2977..64acabf2ff 100644
--- a/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/TransactionalStorageServiceTest.java
+++ b/roda-core/roda-core-tests/src/main/java/org/roda/core/storage/TransactionalStorageServiceTest.java
@@ -30,6 +30,7 @@
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers;
+import org.mockito.Mockito;
import org.roda.core.RodaCoreFactory;
import org.roda.core.TestsHelper;
import org.roda.core.common.iterables.CloseableIterable;
@@ -1948,4 +1949,47 @@ public void testVersioningWithMultipleUpdates() throws RODATransactionException,
Binary rolledBackBinary = mainStorage.getBinary(binaryStoragePath);
testBinaryContent(rolledBackBinary, payload1);
}
+
+ @Test
+ public void testRollbackAfterCommit() throws RODATransactionException, RequestNotValidException,
+ AuthorizationDeniedException, AlreadyExistsException, GenericException, NotFoundException {
+
+ StorageService mockMainStorageService = Mockito.spy(mainStorage);
+
+ // 1.1) start transaction
+ TransactionalContext context1 = transactionManager.beginTestTransaction(mockMainStorageService);
+ StorageService storage1 = context1.transactionalStorageService();
+ // 1.2) create container
+ final StoragePath containerStoragePath = StorageTestUtils.generateRandomContainerStoragePath();
+ storage1.createContainer(containerStoragePath);
+ // 1.3) create a random file
+ final StoragePath binaryStoragePath = StorageTestUtils.generateRandomResourceStoragePathUnder(containerStoragePath);
+ final ContentPayload payload1 = new RandomMockContentPayload();
+ storage1.createBinary(binaryStoragePath, payload1, false);
+
+ // 1.4) create a second random file
+ final StoragePath binaryStoragePath2 = StorageTestUtils
+ .generateRandomResourceStoragePathUnder(containerStoragePath);
+ final ContentPayload payload2 = new RandomMockContentPayload();
+ storage1.createBinary(binaryStoragePath2, payload2, false);
+
+ Mockito.doThrow(new GenericException("Mock exception for testing rollback after commit"))
+ .when(mockMainStorageService).createBinary(Mockito.eq(binaryStoragePath2), Mockito.any(), Mockito.anyBoolean());
+
+ // 1.5) end transaction
+ try {
+ transactionManager.endTransaction(context1.transactionLog().getId());
+ Assert.fail("Expected exception was not thrown");
+ } catch (RODATransactionException e) {
+ LOGGER.info("Caught expected exception: {}", e.getMessage());
+ Assert.assertTrue(mainStorage.exists(binaryStoragePath), "The first binary should exist after commit");
+ Assert.assertTrue(mainStorage.exists(containerStoragePath), "The container should exist after commit");
+ transactionManager.rollbackTransaction(context1.transactionLog().getId());
+ }
+
+ Assert.assertFalse(mainStorage.exists(binaryStoragePath), "The first binary should not exist after rollback");
+ Assert.assertFalse(mainStorage.exists(binaryStoragePath2), "The second binary should not exist after rollback");
+ Assert.assertFalse(mainStorage.exists(containerStoragePath), "The container should not exist after rollback");
+
+ }
}
diff --git a/roda-core/roda-core-tests/src/main/resources/clamd.conf b/roda-core/roda-core-tests/src/main/resources/clamd.conf
new file mode 100644
index 0000000000..5727b141c4
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/clamd.conf
@@ -0,0 +1,2 @@
+TCPSocket 3310
+TCPAddr localhost
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/aip.json b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/aip.json
new file mode 100644
index 0000000000..be68eb9278
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/aip.json
@@ -0,0 +1,74 @@
+{
+ "id": "6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2",
+ "type": "OTHER",
+ "state": "ACTIVE",
+ "permissions": {
+ "users": {
+ "CREATE": [
+ "admin"
+ ],
+ "READ": [
+ "admin"
+ ],
+ "UPDATE": [
+ "admin"
+ ],
+ "DELETE": [
+ "admin"
+ ],
+ "GRANT": [
+ "admin"
+ ]
+ },
+ "groups": {
+ "CREATE": [],
+ "READ": [],
+ "UPDATE": [],
+ "DELETE": [],
+ "GRANT": []
+ }
+ },
+ "descriptiveMetadata": [
+ {
+ "id": "ead2002.xml",
+ "type": "EAD",
+ "version": "2002"
+ }
+ ],
+ "representations": [
+ {
+ "aipId": "6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2",
+ "id": "rep1",
+ "original": true,
+ "representationStates": [],
+ "type": "Other",
+ "hasShallowFiles": false,
+ "createdOn": 1770398258477,
+ "createdBy": "admin",
+ "updatedOn": 1770398264029,
+ "updatedBy": "admin",
+ "descriptiveMetadata": [],
+ "technicalMetadata": []
+ }
+ ],
+ "ingestSIPUUID": "7084c01f-c61e-38f4-802b-37c2f94f90ee",
+ "ingestSIPIds": [
+ "uuid-a9916a17-9826-43fa-8a9b-47b85b55d04c"
+ ],
+ "ingestJobId": "5c2cea51-1a3c-41f4-8942-009ac8dfb28c",
+ "ingestUpdateJobIds": [],
+ "hasShallowFiles": false,
+ "format": {
+ "name": null,
+ "version": null
+ },
+ "relationships": [],
+ "createdOn": 1770398258094,
+ "createdBy": "admin",
+ "updatedOn": 1770398265212,
+ "updatedBy": "admin",
+ "disposal": {
+ "holds": [],
+ "transitiveHolds": []
+ }
+}
\ No newline at end of file
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/descriptive/ead2002.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/descriptive/ead2002.xml
new file mode 100644
index 0000000000..0f870d3c0c
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/descriptive/ead2002.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+ Generated by RODA version 2.0
+
+
+
+
+
+ Sample
+ uuid-a9916a17-9826-43fa-8a9b-47b85b55d04c
+
+
+ English
+
+
+
+
+
+ 2023-04-05
+
+
+
+
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:0c55033a-f4a9-4b13-9115-3ca0f44db195.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:0c55033a-f4a9-4b13-9115-3ca0f44db195.xml
new file mode 100644
index 0000000000..427de53d17
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:0c55033a-f4a9-4b13-9115-3ca0f44db195.xml
@@ -0,0 +1,40 @@
+
+
+
+ URN
+ urn:roda:premis:event:0c55033a-f4a9-4b13-9115-3ca0f44db195
+
+ virus check
+ 2026-02-06T17:17:41.849Z
+
+ Scanned package for malicious programs using ClamAV.
+
+
+ SUCCESS
+
+ The package does not contain any known malicious programs.
+/roda/data/staging-storage/81f8b114-82aa-48a7-90e1-802876261381/aip/6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2: OK
+
+----------- SCAN SUMMARY -----------
+Infected files: 0
+Time: 2.429 sec (0 m 2 s)
+Start Date: 2026:02:06 17:17:39
+End Date: 2026:02:06 17:17:41
+
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.antivirus.AntivirusPlugin@ClamAV 1.5.1/27904/Fri Feb 6 07:25:08 2026
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:10c334ef-a287-4995-9dd2-5070394f32b9.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:10c334ef-a287-4995-9dd2-5070394f32b9.xml
new file mode 100644
index 0000000000..8e596f9047
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:10c334ef-a287-4995-9dd2-5070394f32b9.xml
@@ -0,0 +1,37 @@
+
+
+
+ URN
+ urn:roda:premis:event:10c334ef-a287-4995-9dd2-5070394f32b9
+
+ unpacking
+ 2026-02-06T17:17:38.911Z
+
+ Extracted objects from package in E-ARK SIP 2 format.
+
+
+ SUCCESS
+
+ The SIP has been successfully unpacked.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.EARKSIP2ToAIPPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:TRANSFERRED_RESOURCE:eark_sip_2.0.4.zip
+ source
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:253eec92-0960-4ee1-b0b2-156a6e3ff3c1.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:253eec92-0960-4ee1-b0b2-156a6e3ff3c1.xml
new file mode 100644
index 0000000000..06e8bbcc00
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:253eec92-0960-4ee1-b0b2-156a6e3ff3c1.xml
@@ -0,0 +1,37 @@
+
+
+
+ URN
+ urn:roda:premis:event:253eec92-0960-4ee1-b0b2-156a6e3ff3c1
+
+ ingest start
+ 2026-02-06T17:17:37.628Z
+
+ The ingest process has started.
+
+
+ SUCCESS
+
+ The ingest process has successfully ended.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.v2.ConfigurableIngestPlugin@2.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:TRANSFERRED_RESOURCE:eark_sip_2.0.4.zip
+ source
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:2decab05-8246-4753-9fe2-3617454757bd.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:2decab05-8246-4753-9fe2-3617454757bd.xml
new file mode 100644
index 0000000000..c979690fa6
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:2decab05-8246-4753-9fe2-3617454757bd.xml
@@ -0,0 +1,47 @@
+
+
+
+ URN
+ urn:roda:premis:event:2decab05-8246-4753-9fe2-3617454757bd
+
+ format identification
+ 2026-02-06T17:17:44.496Z
+
+ Identified the object's file formats and versions using Siegfried.
+
+
+ SUCCESS
+
+ File formats were identified and recorded in PREMIS objects.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.characterization.SiegfriedPlugin@1.11.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:FILE:4e69b3bb-406d-3c13-bc4e-386791f27b9a
+ source
+
+
+ URN
+ urn:roda:FILE:e8c301d4-9847-389f-aa28-77a3edb87289
+ source
+
+
+ URN
+ urn:roda:FILE:7ddd5ebc-6cea-3905-a76f-702dd8cacdb2
+ source
+
+
+ URN
+ urn:roda:FILE:dd39e79f-5f4b-3e95-85a7-f68f5e864ab1
+ source
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:36c1f527-febe-44d1-83d3-89f5ee0deffb.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:36c1f527-febe-44d1-83d3-89f5ee0deffb.xml
new file mode 100644
index 0000000000..a7578300c8
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:36c1f527-febe-44d1-83d3-89f5ee0deffb.xml
@@ -0,0 +1,37 @@
+
+
+
+ URN
+ urn:roda:premis:event:36c1f527-febe-44d1-83d3-89f5ee0deffb
+
+ wellformedness check
+ 2026-02-06T17:17:39.018Z
+
+ Checked that the received SIP is well formed, complete and that no unexpected files were included.
+
+
+ SUCCESS
+
+ The SIP was well formed and complete.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.EARKSIP2ToAIPPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:TRANSFERRED_RESOURCE:eark_sip_2.0.4.zip
+ source
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:43a876a5-8249-4dff-82d2-c91a07b0b4f9.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:43a876a5-8249-4dff-82d2-c91a07b0b4f9.xml
new file mode 100644
index 0000000000..8193573871
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:43a876a5-8249-4dff-82d2-c91a07b0b4f9.xml
@@ -0,0 +1,32 @@
+
+
+
+ URN
+ urn:roda:premis:event:43a876a5-8249-4dff-82d2-c91a07b0b4f9
+
+ accession
+ 2026-02-06T17:17:45.047Z
+
+ Added package to the inventory. After this point, the responsibility for the digital content’s preservation is passed on to the repository.
+
+
+ SUCCESS
+
+ The AIP was successfully added to the repository's inventory.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.AutoAcceptSIPPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:6a9c379b-3041-4590-a73f-72c48fca8f93.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:6a9c379b-3041-4590-a73f-72c48fca8f93.xml
new file mode 100644
index 0000000000..f35048cba2
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:6a9c379b-3041-4590-a73f-72c48fca8f93.xml
@@ -0,0 +1,32 @@
+
+
+
+ URN
+ urn:roda:premis:event:6a9c379b-3041-4590-a73f-72c48fca8f93
+
+ message digest calculation
+ 2026-02-06T17:17:43.176Z
+
+ Created base PREMIS objects with file original name and file fixity information (SHA-256).
+
+
+ SUCCESS
+
+ PREMIS objects were successfully created.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.characterization.PremisSkeletonPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:7eba0f70-94f7-4de2-b483-9e69c2866efd.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:7eba0f70-94f7-4de2-b483-9e69c2866efd.xml
new file mode 100644
index 0000000000..64c1b4dc22
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:7eba0f70-94f7-4de2-b483-9e69c2866efd.xml
@@ -0,0 +1,33 @@
+
+
+
+ URN
+ urn:roda:premis:event:7eba0f70-94f7-4de2-b483-9e69c2866efd
+
+ authorization check
+ 2026-02-06T17:17:44.696Z
+
+ User permissions have been checked to ensure that he has sufficient authorization to store the AIP under the desired node of the classification scheme.
+
+
+ SUCCESS
+
+ The user has enough permissions to deposit the AIP under the designated node of the classification scheme
+Done with checking user authorization for AIP 6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.VerifyUserAuthorizationPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:bb46d104-fd4d-4cc3-8bba-a449dde0c908.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:bb46d104-fd4d-4cc3-8bba-a449dde0c908.xml
new file mode 100644
index 0000000000..1c90c28aee
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:bb46d104-fd4d-4cc3-8bba-a449dde0c908.xml
@@ -0,0 +1,37 @@
+
+
+
+ URN
+ urn:roda:premis:event:bb46d104-fd4d-4cc3-8bba-a449dde0c908
+
+ ingest end
+ 2026-02-06T17:17:45.162Z
+
+ The ingest process has ended.
+
+
+ SUCCESS
+
+ The ingest process has successfully ended.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.ingest.v2.ConfigurableIngestPlugin@2.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:TRANSFERRED_RESOURCE:eark_sip_2.0.4.zip
+ source
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:cd37b89a-f9c6-4413-a0e1-6d9aafe6838d.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:cd37b89a-f9c6-4413-a0e1-6d9aafe6838d.xml
new file mode 100644
index 0000000000..e4c25bb959
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/metadata/preservation/urn:roda:premis:event:cd37b89a-f9c6-4413-a0e1-6d9aafe6838d.xml
@@ -0,0 +1,32 @@
+
+
+
+ URN
+ urn:roda:premis:event:cd37b89a-f9c6-4413-a0e1-6d9aafe6838d
+
+ wellformedness check
+ 2026-02-06T17:17:42.095Z
+
+ Checked whether the descriptive metadata is included in the SIP and if this metadata is valid according to the established policy.
+
+
+ SUCCESS
+
+ Descriptive metadata is well formed and complete.
+
+
+
+ URN
+ urn:roda:premis:agent:org.roda.core.plugins.base.preservation.DescriptiveMetadataValidationPlugin@1.0
+
+
+ URN
+ urn:roda:premis:agent:admin
+ implementer
+
+
+ URN
+ urn:roda:AIP:6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2
+ outcome
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/2012-roda-promo-en.pdf b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/2012-roda-promo-en.pdf
new file mode 100644
index 0000000000..93adb386bd
Binary files /dev/null and b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/2012-roda-promo-en.pdf differ
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-black.svg b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-black.svg
new file mode 100644
index 0000000000..a2ca9c1f6c
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-black.svg
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-white.svg b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-white.svg
new file mode 100644
index 0000000000..d9572123e8
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/RODA 2 logo-circle-white.svg
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/subfolder/RODA 2 logo.svg b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/subfolder/RODA 2 logo.svg
new file mode 100644
index 0000000000..0a7105f38c
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/data/folder/subfolder/RODA 2 logo.svg
@@ -0,0 +1,46 @@
+
+
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/2012-roda-promo-en.pdf.json b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/2012-roda-promo-en.pdf.json
new file mode 100644
index 0000000000..269ec6c335
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/2012-roda-promo-en.pdf.json
@@ -0,0 +1 @@
+{"filename":"/roda/data/staging-storage/81f8b114-82aa-48a7-90e1-802876261381/aip/6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2/representations/rep1/data/2012-roda-promo-en.pdf","filesize":5642143,"modified":"2026-02-06T17:17:38Z","errors":"","matches":[{"ns":"pronom","id":"fmt/17","format":"Acrobat PDF 1.3 - Portable Document Format","version":"1.3","mime":"application/pdf","class":"Page Description","basis":"extension match pdf; byte match at [[0 8] [5642137 5]]","warning":""}]}
\ No newline at end of file
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-black.svg.json b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-black.svg.json
new file mode 100644
index 0000000000..c421102b97
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-black.svg.json
@@ -0,0 +1 @@
+{"filename":"/roda/data/staging-storage/81f8b114-82aa-48a7-90e1-802876261381/aip/6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2/representations/rep1/data/folder/RODA 2 logo-circle-black.svg","filesize":1417,"modified":"2026-02-06T17:17:38Z","errors":"","matches":[{"ns":"pronom","id":"fmt/92","format":"Scalable Vector Graphics","version":"1.1","mime":"image/svg+xml","class":"Image (Vector)","basis":"extension match svg; byte match at [[0 19] [236 4] [241 13] [1411 4]]","warning":""}]}
\ No newline at end of file
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-white.svg.json b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-white.svg.json
new file mode 100644
index 0000000000..a12ac8cf0f
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/RODA 2 logo-circle-white.svg.json
@@ -0,0 +1 @@
+{"filename":"/roda/data/staging-storage/81f8b114-82aa-48a7-90e1-802876261381/aip/6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2/representations/rep1/data/folder/RODA 2 logo-circle-white.svg","filesize":1428,"modified":"2026-02-06T17:17:38Z","errors":"","matches":[{"ns":"pronom","id":"fmt/92","format":"Scalable Vector Graphics","version":"1.1","mime":"image/svg+xml","class":"Image (Vector)","basis":"extension match svg; byte match at [[0 19] [236 4] [241 13] [1422 4]]","warning":""}]}
\ No newline at end of file
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/subfolder/RODA 2 logo.svg.json b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/subfolder/RODA 2 logo.svg.json
new file mode 100644
index 0000000000..b112f86630
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/other/Siegfried/folder/subfolder/RODA 2 logo.svg.json
@@ -0,0 +1 @@
+{"filename":"/roda/data/staging-storage/81f8b114-82aa-48a7-90e1-802876261381/aip/6f8cbcb2-8d5b-4dfa-a38b-6a0779d579c2/representations/rep1/data/folder/subfolder/RODA 2 logo.svg","filesize":4132,"modified":"2026-02-06T17:17:38Z","errors":"","matches":[{"ns":"pronom","id":"fmt/92","format":"Scalable Vector Graphics","version":"1.1","mime":"image/svg+xml","class":"Image (Vector)","basis":"extension match svg; byte match at [[0 19] [236 4] [241 13] [4126 4]]","warning":""}]}
\ No newline at end of file
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/subfolder/urn:roda:premis:file:RODA 2 logo.svg.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/subfolder/urn:roda:premis:file:RODA 2 logo.svg.xml
new file mode 100644
index 0000000000..32289ec21b
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/subfolder/urn:roda:premis:file:RODA 2 logo.svg.xml
@@ -0,0 +1,81 @@
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-black.svg.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-black.svg.xml
new file mode 100644
index 0000000000..134843cd4f
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-black.svg.xml
@@ -0,0 +1,81 @@
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-white.svg.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-white.svg.xml
new file mode 100644
index 0000000000..41bdf8b40d
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/folder/urn:roda:premis:file:RODA 2 logo-circle-white.svg.xml
@@ -0,0 +1,81 @@
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:file:2012-roda-promo-en.pdf.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:file:2012-roda-promo-en.pdf.xml
new file mode 100644
index 0000000000..015b0bea76
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:file:2012-roda-promo-en.pdf.xml
@@ -0,0 +1,81 @@
+
+
diff --git a/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:representation:c3f8f330-1459-3c7f-9c2c-472db5c383c6.xml b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:representation:c3f8f330-1459-3c7f-9c2c-472db5c383c6.xml
new file mode 100644
index 0000000000..d52e25690f
--- /dev/null
+++ b/roda-core/roda-core-tests/src/main/resources/corpora/aip/AIP_4/representations/rep1/metadata/preservation/urn:roda:premis:representation:c3f8f330-1459-3c7f-9c2c-472db5c383c6.xml
@@ -0,0 +1,42 @@
+
+
diff --git a/roda-core/roda-core-tests/testng-single.xml b/roda-core/roda-core-tests/testng-single.xml
new file mode 100644
index 0000000000..75fa4f1e97
--- /dev/null
+++ b/roda-core/roda-core-tests/testng-single.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/roda-core/roda-core-tests/testng.xml b/roda-core/roda-core-tests/testng.xml
index aa79fd2552..47c17ed3d7 100644
--- a/roda-core/roda-core-tests/testng.xml
+++ b/roda-core/roda-core-tests/testng.xml
@@ -1,6 +1,9 @@
+
+
+
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/RodaCoreFactory.java b/roda-core/roda-core/src/main/java/org/roda/core/RodaCoreFactory.java
index 9bac9add45..d8269b71ca 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/RodaCoreFactory.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/RodaCoreFactory.java
@@ -1045,7 +1045,11 @@ private static SolrClient instantiateSolr(Path solrHome, boolean writeIsAllowed)
zkChroot = Optional.empty();
}
- CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder(zkHosts, zkChroot).build();
+ int zkClientTimeout = getRodaConfiguration().getInt("core.solr.cloud.zk.client.timeout_ms", 600000);
+ int zkConnectTimeout = getRodaConfiguration().getInt("core.solr.cloud.zk.connect.timeout_ms", 300000);
+ CloudSolrClient cloudSolrClient = new CloudSolrClient.Builder(zkHosts, zkChroot)
+ .withZkClientTimeout(zkClientTimeout, TimeUnit.MILLISECONDS)
+ .withZkConnectTimeout(zkConnectTimeout, TimeUnit.MILLISECONDS).build();
waitForSolrCluster(cloudSolrClient);
@@ -1089,7 +1093,8 @@ private static boolean checkSolrCluster(CloudSolrClient cloudSolrClient)
cloudSolrClient.connect(connectTimeout, TimeUnit.MILLISECONDS);
LOGGER.info("Connected to Solr Cloud");
} catch (TimeoutException e) {
- throw new GenericException("Could not connect to Solr Cloud", e);
+ LOGGER.warn("Timed out waiting for Solr Cloud live nodes (will retry): {}", e.getMessage());
+ return false;
}
ClusterState clusterState = cloudSolrClient.getClusterState();
@@ -1418,7 +1423,7 @@ private static void initializeLdapServer(NodeType nodeType) {
private static void indexUsersAndGroupsFromLDAP() throws GenericException {
for (User user : getModelService().listUsers()) {
getModelService().notifyUserUpdated(user).failOnError();
- if (INSTANTIATE_SOLR) {
+ if (INSTANTIATE_SOLR && getIndexService() != null) {
try {
PremisV3Utils.createOrUpdatePremisUserAgentBinary(user.getName(), getModelService(), getIndexService(), true);
} catch (ValidationException | NotFoundException | RequestNotValidException | AuthorizationDeniedException
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/common/ConfigurableEmailUtility.java b/roda-core/roda-core/src/main/java/org/roda/core/common/ConfigurableEmailUtility.java
index 1fb0758038..64b1019ba6 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/common/ConfigurableEmailUtility.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/common/ConfigurableEmailUtility.java
@@ -30,7 +30,7 @@
public class ConfigurableEmailUtility {
- private static final List DEFAULT_PROPERTIES = Arrays.asList("host", "port", "auth", "starttls.enable");
+ private static final List DEFAULT_PROPERTIES = Arrays.asList("auth", "starttls.enable");
private String protocol;
private String user;
private String password;
@@ -89,6 +89,12 @@ public void sendMail(String recipient, String message) throws MessagingException
private void createSessionParameters() {
boolean hasAuth = false;
+ String port = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.email.port", "1025");
+ props.put("mail.smtp.port", port);
+
+ String host = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.email.host", "localhost");
+ props.put("mail.smtp.host", host);
+
String properties = RodaCoreFactory.getRodaConfigurationAsString("core", "email", "properties");
List propertyList = new ArrayList<>(DEFAULT_PROPERTIES);
if (properties != null) {
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/common/notifications/EmailNotificationProcessor.java b/roda-core/roda-core/src/main/java/org/roda/core/common/notifications/EmailNotificationProcessor.java
index 297ffb3311..21ba379eaf 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/common/notifications/EmailNotificationProcessor.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/common/notifications/EmailNotificationProcessor.java
@@ -98,7 +98,7 @@ public Notification processNotification(ModelService model, final Notification n
for (String recipient : recipients) {
String modifiedBody = getUpdatedMessageBody(model, notification, recipient, template, scope);
- String host = RodaCoreFactory.getRodaConfigurationAsString("core", "email", "host");
+ String host = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.email.host", "127.0.0.1");
if (StringUtils.isNotBlank(host)) {
LOGGER.debug("Sending email ...");
emailUtility.sendMail(recipient, modifiedBody);
@@ -111,7 +111,7 @@ public Notification processNotification(ModelService model, final Notification n
}
} catch (IOException | MessagingException | GenericException e) {
processedNotification.setState(NotificationState.FAILED);
- LOGGER.debug("Error sending e-mail: {}", e.getMessage());
+ LOGGER.error("Error sending e-mail: {}", e.getMessage());
}
return processedNotification;
}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/config/ConfigurationManager.java b/roda-core/roda-core/src/main/java/org/roda/core/config/ConfigurationManager.java
index d2c02f8bac..65351893aa 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/config/ConfigurationManager.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/config/ConfigurationManager.java
@@ -409,6 +409,9 @@ public List getRodaConfigurationAsList(String... keyParts) {
public String getConfigurationString(String key, String defaultValue) {
String envKey = "RODA_" + key.toUpperCase().replace('.', '_');
String value = System.getenv(envKey);
+ if (value == null) {
+ value = System.getProperty(envKey);
+ }
if (value == null) {
value = rodaConfiguration.getString(key, defaultValue);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/index/IndexModelObserver.java b/roda-core/roda-core/src/main/java/org/roda/core/index/IndexModelObserver.java
index ef1052f6d0..0647dba256 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/index/IndexModelObserver.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/index/IndexModelObserver.java
@@ -33,6 +33,8 @@
import org.roda.core.data.exceptions.ReturnWithExceptions;
import org.roda.core.data.utils.JsonUtils;
import org.roda.core.data.v2.IsModelObject;
+import org.roda.core.data.v2.IsRODAObject;
+import org.roda.core.data.v2.LiteRODAObject;
import org.roda.core.data.v2.common.OptionalWithCause;
import org.roda.core.data.v2.disposal.confirmation.DisposalConfirmation;
import org.roda.core.data.v2.disposal.schedule.DisposalSchedule;
@@ -79,8 +81,16 @@
import org.roda.core.index.schema.collections.RiskCollection;
import org.roda.core.index.utils.IterableIndexResult;
import org.roda.core.index.utils.SolrUtils;
+import org.roda.core.model.LiteRODAObjectFactory;
import org.roda.core.model.ModelObserver;
import org.roda.core.model.ModelService;
+import org.roda.core.model.lites.ParsedAIPLite;
+import org.roda.core.model.lites.ParsedDIPFileLite;
+import org.roda.core.model.lites.ParsedDIPLite;
+import org.roda.core.model.lites.ParsedFileLite;
+import org.roda.core.model.lites.ParsedLite;
+import org.roda.core.model.lites.ParsedPreservationMetadataLite;
+import org.roda.core.model.lites.ParsedRepresentationLite;
import org.roda.core.storage.Binary;
import org.roda.core.storage.fs.FSUtils;
import org.roda.core.util.IdUtils;
@@ -404,6 +414,17 @@ public ReturnWithExceptions aipUpdated(AIP aip) {
return ret;
}
+ @Override
+ public ReturnWithExceptions aipOnHoldStatusUpdated(AIP aip, boolean status) {
+ ReturnWithExceptions ret = new ReturnWithExceptions<>(this);
+
+ // change AIP
+ Map updatedFields = new HashMap<>();
+ updatedFields.put(RodaConstants.AIP_DISPOSAL_HOLD_STATUS, status);
+ SolrUtils.update(index, IndexedAIP.class, aip.getId(), updatedFields, (ModelObserver) this).addTo(ret);
+ return ret;
+ }
+
@Override
public ReturnWithExceptions aipUpdatedOn(AIP aip) {
ReturnWithExceptions ret = new ReturnWithExceptions<>(this);
@@ -1488,4 +1509,70 @@ public ReturnWithExceptions disposalConfirmationDeleted(Str
return deleteDocumentFromIndex(DisposalConfirmation.class, confirmationId);
}
+ public ReturnWithExceptions liteRODAObjectCreated(LiteRODAObject liteRODAObject) {
+ ReturnWithExceptions ret = new ReturnWithExceptions<>(this);
+ OptionalWithCause liteObject = LiteRODAObjectFactory.get(model, liteRODAObject);
+ if (liteObject.isPresent()) {
+ IsRODAObject obj = liteObject.get();
+ if (obj instanceof AIP aip) {
+ ret.add(aipCreated(aip));
+ } else if (obj instanceof Representation rep) {
+ ret.add(representationCreated(rep));
+ } else if (obj instanceof File file) {
+ ret.add(fileCreated(file));
+ } else if (obj instanceof DIP dip) {
+ ret.add(dipCreated(dip, true));
+ } else if (obj instanceof DIPFile dipFile) {
+ ret.add(dipFileCreated(dipFile));
+ } else if (obj instanceof PreservationMetadata pm) {
+ ret.add(preservationMetadataCreated(pm));
+ }
+ }
+ return ret;
+ }
+
+ public ReturnWithExceptions liteRODAObjectUpdated(LiteRODAObject liteRODAObject) {
+ ReturnWithExceptions ret = new ReturnWithExceptions<>(this);
+ OptionalWithCause liteObject = LiteRODAObjectFactory.get(model, liteRODAObject);
+ if (liteObject.isPresent()) {
+ IsRODAObject obj = liteObject.get();
+ if (obj instanceof AIP aip) {
+ ret.add(aipUpdated(aip));
+ } else if (obj instanceof Representation rep) {
+ ret.add(representationUpdated(rep));
+ } else if (obj instanceof File file) {
+ ret.add(fileUpdated(file));
+ } else if (obj instanceof DIP dip) {
+ ret.add(dipUpdated(dip, true));
+ } else if (obj instanceof DIPFile dipFile) {
+ ret.add(dipFileUpdated(dipFile));
+ } else if (obj instanceof PreservationMetadata pm) {
+ ret.add(preservationMetadataUpdated(pm));
+ }
+ }
+ return ret;
+ }
+
+ public ReturnWithExceptions liteRODAObjectDeleted(LiteRODAObject liteRODAObject) {
+ ReturnWithExceptions ret = new ReturnWithExceptions<>(this);
+ OptionalWithCause parsed = ParsedLite.parse(liteRODAObject);
+ if (parsed.isPresent()) {
+ ParsedLite lite = parsed.get();
+ if (lite instanceof ParsedAIPLite aip) {
+ ret.add(aipDeleted(aip.getId(), true));
+
+ } else if (lite instanceof ParsedRepresentationLite rep) {
+ ret.add(representationDeleted(rep.getAipId(), rep.getId(), true));
+ } else if (lite instanceof ParsedFileLite file) {
+ ret.add(fileDeleted(file.getAipId(), file.getRepresentationId(), file.getDirectoryPath(), file.getId(), true));
+ } else if (lite instanceof ParsedDIPLite dip) {
+ ret.add(dipDeleted(dip.getId(), true));
+ } else if (lite instanceof ParsedDIPFileLite dipFile) {
+ ret.add(dipFileDeleted(dipFile.getId(), dipFile.getDirectoryPath(), dipFile.getFileId()));
+ } else if (lite instanceof ParsedPreservationMetadataLite pm) {
+ ret.add(preservationMetadataDeleted(new PreservationMetadata(pm.getId(), pm.getType())));
+ }
+ }
+ return ret;
+ }
}
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 eb3c06bd5f..7da4af2b04 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
@@ -12,9 +12,12 @@
import static org.roda.core.common.DownloadUtils.ZIP_PATH_DELIMITER;
import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.SequenceInputStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
@@ -43,12 +46,15 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.roda.core.RodaCoreFactory;
import org.roda.core.common.JwtUtils;
import org.roda.core.common.PremisV3Utils;
+import org.roda.core.common.ProvidesInputStream;
import org.roda.core.common.ReturnWithExceptionsWrapper;
import org.roda.core.common.dips.DIPUtils;
import org.roda.core.common.iterables.CloseableIterable;
@@ -165,6 +171,7 @@
import org.roda.core.storage.EmptyClosableIterable;
import org.roda.core.storage.Entity;
import org.roda.core.storage.ExternalFileManifestContentPayload;
+import org.roda.core.storage.InputStreamContentPayload;
import org.roda.core.storage.JsonContentPayload;
import org.roda.core.storage.Resource;
import org.roda.core.storage.StorageService;
@@ -176,6 +183,9 @@
import org.roda.core.util.HTTPUtility;
import org.roda.core.util.IdUtils;
import org.roda.core.util.RESTClientUtility;
+import org.roda.core.config.SpringContext;
+import org.roda.core.repository.job.JobRepository;
+import org.roda.core.repository.job.ReportRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -194,13 +204,42 @@ public class DefaultModelService implements ModelService {
private final StorageService storage;
private final EventsManager eventsManager;
private final NodeType nodeType;
+ // Observer
+ private final List observers;
private String instanceId = "";
private Object logFileLock = new Object();
-
private long entryLogLineNumber = -1;
- // Observer
- private final List observers;
+ // Lazy-loaded JPA repositories for hybrid Job/Report persistence
+ private JobRepository jobRepository;
+ private ReportRepository reportRepository;
+
+ /**
+ * Lazily retrieves the JobRepository bean from Spring context.
+ */
+ private JobRepository getJobRepository() {
+ if (jobRepository == null && SpringContext.isContextInitialized()) {
+ jobRepository = SpringContext.getBean(JobRepository.class);
+ }
+ return jobRepository;
+ }
+
+ /**
+ * Lazily retrieves the ReportRepository bean from Spring context.
+ */
+ private ReportRepository getReportRepository() {
+ if (reportRepository == null && SpringContext.isContextInitialized()) {
+ reportRepository = SpringContext.getBean(ReportRepository.class);
+ }
+ return reportRepository;
+ }
+
+ /**
+ * Checks if the JPA repositories are available (Spring context is initialized).
+ */
+ private boolean isJpaAvailable() {
+ return SpringContext.isContextInitialized();
+ }
public DefaultModelService(StorageService storage, EventsManager eventsManager, NodeType nodeType,
String instanceId) {
@@ -216,6 +255,18 @@ public DefaultModelService(StorageService storage, EventsManager eventsManager,
}
}
+ private static void clearSpecificIndexes(IndexService index, Class objectClass,
+ IsModelObject rodaObject) throws AuthorizationDeniedException {
+ if (AIP.class.equals(objectClass)) {
+ List ids = Arrays.asList(rodaObject.getId());
+ index.delete(IndexedRepresentation.class,
+ new Filter(new OneOfManyFilterParameter(RodaConstants.REPRESENTATION_AIP_ID, ids)));
+ index.delete(IndexedFile.class, new Filter(new OneOfManyFilterParameter(RodaConstants.FILE_AIP_ID, ids)));
+ index.delete(IndexedPreservationEvent.class,
+ new Filter(new OneOfManyFilterParameter(RodaConstants.PRESERVATION_EVENT_AIP_ID, ids)));
+ }
+ }
+
private void ensureAllContainersExist() {
try {
createContainerIfNotExists(RodaConstants.STORAGE_CONTAINER_AIP);
@@ -618,6 +669,14 @@ public AIP updateAIP(AIP aip, String updatedBy)
return aip;
}
+ @Override
+ public AIP updateAIPOnHoldStatus(AIP aip, boolean status) throws AuthorizationDeniedException, GenericException {
+ RodaCoreFactory.checkIfWriteIsAllowedAndIfFalseThrowException(nodeType);
+
+ notifyAipOnHoldStatusUpdated(aip, status).failOnError();
+ return aip;
+ }
+
@Override
public AIP updateAIPState(AIP aip, String updatedBy)
throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
@@ -2862,17 +2921,71 @@ public void createOrUpdateJob(Job job)
if (job.getInstanceId() == null) {
job.setInstanceId(RODAInstanceUtils.getLocalInstanceIdentifier());
}
- // create or update job in storage
+
+ // Check if JPA is available and determine persistence strategy
+ if (isJpaAvailable() && getJobRepository() != null) {
+ if (Job.isFinalState(job.getState())) {
+ // Job is in final state - flush to storage and remove from DB
+ flushJobToStorage(job);
+ } else {
+ // Job is running - save to database only
+ getJobRepository().save(job);
+ }
+ } else {
+ // Fallback to storage-only persistence
+ String jobAsJson = JsonUtils.getJsonFromObject(job);
+ StoragePath jobPath = ModelUtils.getJobStoragePath(job.getId());
+ storage.updateBinaryContent(jobPath, new StringContentPayload(jobAsJson), false, true, false, null);
+ }
+ // index it
+ notifyJobCreatedOrUpdated(job, false).failOnError();
+ }
+
+ /**
+ * Flushes a job and its reports from the database to the file storage.
+ * This method is called when a job reaches a final state.
+ */
+ private void flushJobToStorage(Job job)
+ throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
+ // Write job to storage
String jobAsJson = JsonUtils.getJsonFromObject(job);
StoragePath jobPath = ModelUtils.getJobStoragePath(job.getId());
storage.updateBinaryContent(jobPath, new StringContentPayload(jobAsJson), false, true, false, null);
- // index it
- notifyJobCreatedOrUpdated(job, false).failOnError();
+
+ // Flush all reports for this job from DB to storage
+ if (getReportRepository() != null) {
+ List dbReports = getReportRepository().findByJobId(job.getId());
+ for (Report report : dbReports) {
+ String reportAsJson = JsonUtils.getJsonFromObject(report);
+ StoragePath reportPath = ModelUtils.getJobReportStoragePath(report.getJobId(), report.getId());
+ storage.updateBinaryContent(reportPath, new StringContentPayload(reportAsJson), false, true, false, null);
+ }
+ // Delete reports from DB
+ getReportRepository().deleteByJobId(job.getId());
+ }
+
+ // Delete job from DB
+ JobRepository jobRepo = getJobRepository();
+ if (jobRepo != null && jobRepo.existsById(job.getId())) {
+ jobRepo.deleteById(job.getId());
+ }
}
@Override
public Job retrieveJob(String jobId)
throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
+ // Try to fetch from database first (for running jobs)
+ if (isJpaAvailable()) {
+ JobRepository jobRepo = getJobRepository();
+ if (jobRepo != null) {
+ Optional dbJob = jobRepo.findById(jobId);
+ if (dbJob.isPresent()) {
+ return dbJob.get();
+ }
+ }
+ }
+
+ // Fallback to storage (for completed/archived jobs)
StoragePath jobPath = ModelUtils.getJobStoragePath(jobId);
Binary binary = storage.getBinary(jobPath);
Job ret;
@@ -2888,6 +3001,21 @@ public Job retrieveJob(String jobId)
public CloseableIterable> listJobReports(String jobId)
throws RequestNotValidException, AuthorizationDeniedException, NotFoundException, GenericException {
+ // Check if job exists in database (running job)
+ if (isJpaAvailable()) {
+ JobRepository jobRepo = getJobRepository();
+ ReportRepository reportRepo = getReportRepository();
+ if (jobRepo != null && reportRepo != null && jobRepo.existsById(jobId)) {
+ // Return reports from database
+ List dbReports = reportRepo.findByJobId(jobId);
+ List> wrappedReports = dbReports.stream()
+ .map(OptionalWithCause::of)
+ .collect(Collectors.toList());
+ return CloseableIterables.fromList(wrappedReports);
+ }
+ }
+
+ // Fallback to storage
final CloseableIterable resourcesIterable = storage
.listResourcesUnderContainer(ModelUtils.getJobReportsStoragePath(jobId), false);
return ResourceParseUtils.convert(getStorage(), resourcesIterable, Report.class);
@@ -2898,10 +3026,40 @@ public void deleteJob(String jobId)
throws NotFoundException, GenericException, AuthorizationDeniedException, RequestNotValidException {
RodaCoreFactory.checkIfWriteIsAllowedAndIfFalseThrowException(nodeType);
- StoragePath jobPath = ModelUtils.getJobStoragePath(jobId);
+ boolean deletedFromDb = false;
- // remove it from storage
- storage.deleteResource(jobPath);
+ // Try to delete from database first (for running jobs)
+ if (isJpaAvailable()) {
+ JobRepository jobRepo = getJobRepository();
+ ReportRepository reportRepo = getReportRepository();
+ if (jobRepo != null && jobRepo.existsById(jobId)) {
+ // Delete reports from DB
+ if (reportRepo != null) {
+ reportRepo.deleteByJobId(jobId);
+ }
+ // Delete job from DB
+ jobRepo.deleteById(jobId);
+ deletedFromDb = true;
+ }
+ }
+
+ // Also try to delete from storage (for archived jobs or cleanup)
+ try {
+ StoragePath jobPath = ModelUtils.getJobStoragePath(jobId);
+ storage.deleteResource(jobPath);
+ // Also try to delete job reports directory from storage
+ try {
+ StoragePath reportsPath = ModelUtils.getJobReportsStoragePath(jobId);
+ storage.deleteResource(reportsPath);
+ } catch (NotFoundException e) {
+ // Reports directory may not exist, ignore
+ }
+ } catch (NotFoundException e) {
+ // If not found in storage and also not deleted from DB, propagate the exception
+ if (!deletedFromDb) {
+ throw e;
+ }
+ }
// remove it from index
notifyJobDeleted(jobId).failOnError();
@@ -2910,6 +3068,18 @@ public void deleteJob(String jobId)
@Override
public Report retrieveJobReport(String jobId, String jobReportId)
throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException {
+ // Try to fetch from database first (for running jobs)
+ if (isJpaAvailable()) {
+ ReportRepository reportRepo = getReportRepository();
+ if (reportRepo != null) {
+ Optional dbReport = reportRepo.findById(jobReportId);
+ if (dbReport.isPresent()) {
+ return dbReport.get();
+ }
+ }
+ }
+
+ // Fallback to storage
StoragePath jobReportPath = ModelUtils.getJobReportStoragePath(jobId, jobReportId);
Binary binary = storage.getBinary(jobReportPath);
Report ret;
@@ -2937,14 +3107,39 @@ public void createOrUpdateJobReport(Report jobReport, Job cachedJob)
jobReport.setInstanceId(RODAInstanceUtils.getLocalInstanceIdentifier());
- // create job report in storage
+ // Handle ID change
+ String newId = IdUtils.getJobReportId(jobReport.getJobId(), jobReport.getSourceObjectId(),
+ jobReport.getOutcomeObjectId());
+ String oldId = null;
+ if (!newId.equals(jobReport.getId())) {
+ oldId = jobReport.getId();
+ jobReport.setId(newId);
+ }
+
+ // Check if job exists in database (running job) - use DB for reports
+ if (isJpaAvailable()) {
+ JobRepository jobRepo = getJobRepository();
+ ReportRepository reportRepo = getReportRepository();
+ if (jobRepo != null && reportRepo != null && jobRepo.existsById(jobReport.getJobId())) {
+ try {
+ // Delete old report from DB if ID changed
+ if (oldId != null && reportRepo.existsById(oldId)) {
+ reportRepo.deleteById(oldId);
+ notifyJobReportDeleted(oldId);
+ }
+ // Save to database
+ reportRepo.save(jobReport);
+ // index it
+ notifyJobReportCreatedOrUpdated(jobReport, cachedJob).failOnError();
+ } catch (Exception e) {
+ LOGGER.error("Error creating/updating job report in database", e);
+ }
+ return;
+ }
+ }
+ // Fallback to storage persistence
try {
- // if job report changed id, set it and remove old report
- String newId = IdUtils.getJobReportId(jobReport.getJobId(), jobReport.getSourceObjectId(),
- jobReport.getOutcomeObjectId());
- if (!newId.equals(jobReport.getId())) {
- String oldId = jobReport.getId();
- jobReport.setId(newId);
+ if (oldId != null) {
storage.deleteResource(ModelUtils.getJobReportStoragePath(jobReport.getJobId(), oldId));
notifyJobReportDeleted(oldId);
}
@@ -2967,14 +3162,39 @@ public void createOrUpdateJobReport(Report jobReport, IndexedJob indexJob)
jobReport.setInstanceId(RODAInstanceUtils.getLocalInstanceIdentifier());
- // create job report in storage
+ // Handle ID change
+ String newId = IdUtils.getJobReportId(jobReport.getJobId(), jobReport.getSourceObjectId(),
+ jobReport.getOutcomeObjectId());
+ String oldId = null;
+ if (!newId.equals(jobReport.getId())) {
+ oldId = jobReport.getId();
+ jobReport.setId(newId);
+ }
+
+ // Check if job exists in database (running job) - use DB for reports
+ if (isJpaAvailable()) {
+ JobRepository jobRepo = getJobRepository();
+ ReportRepository reportRepo = getReportRepository();
+ if (jobRepo != null && reportRepo != null && jobRepo.existsById(jobReport.getJobId())) {
+ try {
+ // Delete old report from DB if ID changed
+ if (oldId != null && reportRepo.existsById(oldId)) {
+ reportRepo.deleteById(oldId);
+ notifyJobReportDeleted(oldId);
+ }
+ // Save to database
+ reportRepo.save(jobReport);
+ // index it
+ notifyJobReportCreatedOrUpdated(jobReport, indexJob).failOnError();
+ } catch (Exception e) {
+ LOGGER.error("Error creating/updating job report in database", e);
+ }
+ return;
+ }
+ }
+ // Fallback to storage persistence
try {
- // if job report changed id, set it and remove old report
- String newId = IdUtils.getJobReportId(jobReport.getJobId(), jobReport.getSourceObjectId(),
- jobReport.getOutcomeObjectId());
- if (!newId.equals(jobReport.getId())) {
- String oldId = jobReport.getId();
- jobReport.setId(newId);
+ if (oldId != null) {
storage.deleteResource(ModelUtils.getJobReportStoragePath(jobReport.getJobId(), oldId));
notifyJobReportDeleted(oldId);
}
@@ -3671,6 +3891,22 @@ public void createSubmission(Path submissionPath, String aipId) throws AlreadyEx
storage.createBinary(submissionStoragePath, new FSPathContentPayload(submissionPath), false);
}
+ @Override
+ public void createMetsFile(String aipId, String repId, ContentPayload metsPayload) throws RequestNotValidException,
+ GenericException, AlreadyExistsException, AuthorizationDeniedException, NotFoundException {
+ RodaCoreFactory.checkIfWriteIsAllowedAndIfFalseThrowException(nodeType);
+
+ StoragePath metsOutPut = null;
+
+ if (repId != null) {
+ metsOutPut = ModelUtils.getMetsStoragePath(aipId, repId);
+ } else {
+ metsOutPut = ModelUtils.getMetsStoragePath(aipId, null);
+ }
+ storage.createBinary(metsOutPut, metsPayload, false);
+
+ }
+
private Directory getDocumentationDirectory(String aipId)
throws RequestNotValidException, NotFoundException, GenericException, AuthorizationDeniedException {
return storage.getDirectory(ModelUtils.getDocumentationStoragePath(aipId));
@@ -3845,7 +4081,35 @@ public CloseableIterable> list(Cla
} else if (DescriptiveMetadata.class.equals(objectClass)) {
ret = listDescriptiveMetadata();
} else if (Report.class.equals(objectClass)) {
- ret = ResourceParseUtils.convert(getStorage(), listReportResources(), objectClass);
+ // Include both DB reports (for running jobs) and storage reports (for completed jobs)
+ CloseableIterable> storageReports = ResourceParseUtils.convert(getStorage(),
+ listReportResources(), Report.class);
+ if (isJpaAvailable() && getReportRepository() != null) {
+ List dbReports = getReportRepository().findAll();
+ List> wrappedDbReports = dbReports.stream()
+ .map(OptionalWithCause::of)
+ .collect(Collectors.toList());
+ CloseableIterable> dbIterable = CloseableIterables.fromList(wrappedDbReports);
+ ret = CloseableIterables.concat(dbIterable, storageReports);
+ } else {
+ ret = storageReports;
+ }
+ } else if (Job.class.equals(objectClass)) {
+ // Include both DB jobs (running) and storage jobs (completed/archived)
+ StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
+ final CloseableIterable resourcesIterable = storage.listResourcesUnderContainer(containerPath, false);
+ CloseableIterable> storageJobs = ResourceParseUtils.convert(getStorage(),
+ resourcesIterable, Job.class);
+ if (isJpaAvailable() && getJobRepository() != null) {
+ List dbJobs = getJobRepository().findAll();
+ List> wrappedDbJobs = dbJobs.stream()
+ .map(OptionalWithCause::of)
+ .collect(Collectors.toList());
+ CloseableIterable> dbIterable = CloseableIterables.fromList(wrappedDbJobs);
+ ret = CloseableIterables.concat(dbIterable, storageJobs);
+ } else {
+ ret = storageJobs;
+ }
} else {
StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
final CloseableIterable resourcesIterable = storage.listResourcesUnderContainer(containerPath, false);
@@ -3880,12 +4144,42 @@ public CloseableIterable> storageReports = ResourceParseUtils.convertLite(getStorage(),
+ listReportResources(), objectClass);
+ if (isJpaAvailable() && getReportRepository() != null) {
+ List dbReports = getReportRepository().findAll();
+ List> wrappedDbReports = dbReports.stream()
+ .map(OptionalWithCause::of)
+ .collect(Collectors.toList());
+ CloseableIterable> dbIterable = LiteRODAObjectFactory
+ .transformIntoLite(CloseableIterables.fromList(wrappedDbReports));
+ ret = CloseableIterables.concat(dbIterable, storageReports);
+ } else {
+ ret = storageReports;
+ }
/*
* } else if (DisposalConfirmation.class.equals(objectClass)) { ret =
* ResourceParseUtils.convertLite(getStorage(),
* ResourceListUtils.listDisposalConfirmationResources(storage), objectClass);
*/
+ } else if (Job.class.equals(objectClass)) {
+ // Include both DB jobs (running) and storage jobs (completed/archived)
+ StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
+ final CloseableIterable resourcesIterable = storage.listResourcesUnderContainer(containerPath, false);
+ CloseableIterable> storageJobs = ResourceParseUtils.convertLite(getStorage(),
+ resourcesIterable, objectClass);
+ if (isJpaAvailable() && getJobRepository() != null) {
+ List dbJobs = getJobRepository().findAll();
+ List> wrappedDbJobs = dbJobs.stream()
+ .map(OptionalWithCause::of)
+ .collect(Collectors.toList());
+ CloseableIterable> dbIterable = LiteRODAObjectFactory
+ .transformIntoLite(CloseableIterables.fromList(wrappedDbJobs));
+ ret = CloseableIterables.concat(dbIterable, storageJobs);
+ } else {
+ ret = storageJobs;
+ }
} else {
StoragePath containerPath = ModelUtils.getContainerPath(objectClass);
final CloseableIterable resourcesIterable = storage.listResourcesUnderContainer(containerPath, false);
@@ -4530,51 +4824,131 @@ public DisposalConfirmation retrieveDisposalConfirmation(String disposalConfirma
}
@Override
- public void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold)
- throws GenericException, RequestNotValidException {
- StoragePath confirmationStoragePath = ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId);
- Path confirmationPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), confirmationStoragePath);
+ public void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold) throws GenericException,
+ RequestNotValidException, AuthorizationDeniedException, NotFoundException, AlreadyExistsException {
+ StoragePath confirmationStoragePath = ModelUtils
+ .getDisposalHoldsFromDisposalConfirmationStoragePath(disposalConfirmationId);
+
+ if (!storage.exists(confirmationStoragePath)) {
+ storage.createBinary(confirmationStoragePath, new StringContentPayload(JsonUtils.getJsonFromObject(disposalHold)),
+ false);
+ } else {
+ Binary binary = storage.getBinary(confirmationStoragePath);
- Path file = FSUtils.createFile(confirmationPath,
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_HOLDS_FILENAME, true, true);
+ ProvidesInputStream streamProvider = () -> {
+ // We let the storage framework handle closing this when it finishes reading
+ InputStream originalStream = binary.getContent().createInputStream();
+ String jsonFromObject = JsonUtils.getJsonFromObject(disposalHold);
+ byte[] pojoBytes = jsonFromObject.getBytes(StandardCharsets.UTF_8);
+ byte[] newline = "\n".getBytes(StandardCharsets.UTF_8);
- JsonUtils.appendObjectToFile(disposalHold, file);
+ InputStream newlineStream = new ByteArrayInputStream(newline);
+ InputStream pojoStream = new ByteArrayInputStream(pojoBytes);
+
+ InputStream firstJoin = new SequenceInputStream(originalStream, newlineStream);
+ return new SequenceInputStream(firstJoin, pojoStream);
+ };
+
+ storage.updateBinaryContent(confirmationStoragePath, new InputStreamContentPayload(streamProvider), false, false,
+ false, null);
+ }
}
@Override
public void addDisposalHoldTransitiveEntry(String disposalConfirmationId, DisposalHold transitiveDisposalHold)
- throws RequestNotValidException, GenericException {
- StoragePath confirmationStoragePath = ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId);
- Path confirmationPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), confirmationStoragePath);
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException {
+ StoragePath confirmationStoragePath = ModelUtils
+ .getDisposalTransitiveHoldsFromDisposalConfirmationStoragePath(disposalConfirmationId);
- Path file = FSUtils.createFile(confirmationPath,
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_TRANSITIVE_HOLDS_FILENAME, true, true);
+ if (!storage.exists(confirmationStoragePath)) {
+ storage.createBinary(confirmationStoragePath,
+ new StringContentPayload(JsonUtils.getJsonFromObject(transitiveDisposalHold)), false);
+ } else {
+ Binary binary = storage.getBinary(confirmationStoragePath);
- JsonUtils.appendObjectToFile(transitiveDisposalHold, file);
+ ProvidesInputStream streamProvider = () -> {
+ // We let the storage framework handle closing this when it finishes reading
+ InputStream originalStream = binary.getContent().createInputStream();
+ String jsonFromObject = JsonUtils.getJsonFromObject(transitiveDisposalHold);
+ byte[] pojoBytes = jsonFromObject.getBytes(StandardCharsets.UTF_8);
+ byte[] newline = "\n".getBytes(StandardCharsets.UTF_8);
+
+ InputStream newlineStream = new ByteArrayInputStream(newline);
+ InputStream pojoStream = new ByteArrayInputStream(pojoBytes);
+
+ InputStream firstJoin = new SequenceInputStream(originalStream, newlineStream);
+ return new SequenceInputStream(firstJoin, pojoStream);
+ };
+
+ storage.updateBinaryContent(confirmationStoragePath, new InputStreamContentPayload(streamProvider), false, false,
+ false, null);
+ }
}
@Override
public void addDisposalScheduleEntry(String disposalConfirmationId, DisposalSchedule disposalSchedule)
- throws RequestNotValidException, GenericException {
- StoragePath confirmationStoragePath = ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId);
- Path confirmationPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), confirmationStoragePath);
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException {
+
+ StoragePath confirmationStoragePath = ModelUtils
+ .getDisposalSchedulesFromDisposalConfirmationStoragePath(disposalConfirmationId);
+
+ if (!storage.exists(confirmationStoragePath)) {
+ storage.createBinary(confirmationStoragePath,
+ new StringContentPayload(JsonUtils.getJsonFromObject(disposalSchedule)), false);
+ } else {
+ Binary binary = storage.getBinary(confirmationStoragePath);
+
+ ProvidesInputStream streamProvider = () -> {
+ // We let the storage framework handle closing this when it finishes reading
+ InputStream originalStream = binary.getContent().createInputStream();
+ String jsonFromObject = JsonUtils.getJsonFromObject(disposalSchedule);
+ byte[] pojoBytes = jsonFromObject.getBytes(StandardCharsets.UTF_8);
+ byte[] newline = "\n".getBytes(StandardCharsets.UTF_8);
+
+ InputStream newlineStream = new ByteArrayInputStream(newline);
+ InputStream pojoStream = new ByteArrayInputStream(pojoBytes);
- Path file = FSUtils.createFile(confirmationPath,
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_SCHEDULES_FILENAME, true, true);
+ InputStream firstJoin = new SequenceInputStream(originalStream, newlineStream);
+ return new SequenceInputStream(firstJoin, pojoStream);
+ };
- JsonUtils.appendObjectToFile(disposalSchedule, file);
+ storage.updateBinaryContent(confirmationStoragePath, new InputStreamContentPayload(streamProvider), false, false,
+ false, null);
+ }
}
@Override
public void addAIPEntry(String disposalConfirmationId, DisposalConfirmationAIPEntry entry)
- throws RequestNotValidException, GenericException {
- StoragePath confirmationStoragePath = ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId);
- Path confirmationPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), confirmationStoragePath);
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException {
+ StoragePath confirmationStoragePath = ModelUtils
+ .getDisposalAipsFromDisposalConfirmationStoragePath(disposalConfirmationId);
+
+ if (!storage.exists(confirmationStoragePath)) {
+ storage.createBinary(confirmationStoragePath, new StringContentPayload(JsonUtils.getJsonFromObject(entry)),
+ false);
+ } else {
+ Binary binary = storage.getBinary(confirmationStoragePath);
- Path file = FSUtils.createFile(confirmationPath,
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_AIPS_FILENAME, true, true);
+ ProvidesInputStream streamProvider = () -> {
+ // We let the storage framework handle closing this when it finishes reading
+ InputStream originalStream = binary.getContent().createInputStream();
+ String jsonFromObject = JsonUtils.getJsonFromObject(entry);
+ byte[] pojoBytes = jsonFromObject.getBytes(StandardCharsets.UTF_8);
+ byte[] newline = "\n".getBytes(StandardCharsets.UTF_8);
- JsonUtils.appendObjectToFile(entry, file);
+ InputStream newlineStream = new ByteArrayInputStream(newline);
+ InputStream pojoStream = new ByteArrayInputStream(pojoBytes);
+
+ InputStream firstJoin = new SequenceInputStream(originalStream, newlineStream);
+ return new SequenceInputStream(firstJoin, pojoStream);
+ };
+
+ storage.updateBinaryContent(confirmationStoragePath, new InputStreamContentPayload(streamProvider), false, false,
+ false, null);
+ }
}
@Override
@@ -4726,6 +5100,10 @@ public DisposalRule retrieveDisposalRule(String disposalRuleId)
return ret;
}
+ /************************************
+ * Disposal bin related
+ ************************************/
+
@Override
public DisposalRules listDisposalRules()
throws RequestNotValidException, GenericException, AuthorizationDeniedException, IOException {
@@ -4746,10 +5124,6 @@ public DisposalRules listDisposalRules()
return disposalRules;
}
- /************************************
- * Disposal bin related
- ************************************/
-
/************************************
* Distributed instances system related
************************************/
@@ -5060,6 +5434,18 @@ private ReturnWithExceptionsWrapper notifyObserversSafely(Function observer.liteRODAObjectCreated(object));
+ }
+
+ public ReturnWithExceptionsWrapper notifyLiteRodaObjectUpdated(LiteRODAObject object) {
+ return notifyObserversSafely(observer -> observer.liteRODAObjectUpdated(object));
+ }
+
+ public ReturnWithExceptionsWrapper notifyLiteRodaObjectDeleted(LiteRODAObject object) {
+ return notifyObserversSafely(observer -> observer.liteRODAObjectUpdated(object));
+ }
+
@Override
public ReturnWithExceptionsWrapper notifyAipCreated(AIP aip) {
return notifyObserversSafely(observer -> observer.aipCreated(aip));
@@ -5075,6 +5461,11 @@ public ReturnWithExceptionsWrapper notifyAipUpdatedOnChanged(AIP aip) {
return notifyObserversSafely(observer -> observer.aipUpdatedOn(aip));
}
+ @Override
+ public ReturnWithExceptionsWrapper notifyAipOnHoldStatusUpdated(AIP aip, boolean status) {
+ return notifyObserversSafely(observer -> observer.aipOnHoldStatusUpdated(aip, status));
+ }
+
@Override
public ReturnWithExceptionsWrapper notifyAipDestroyed(AIP aip) {
return notifyObserversSafely(observer -> observer.aipDestroyed(aip));
@@ -5631,26 +6022,27 @@ private void reindexResource(IndexService index, Resource resource)
}
}
- private static void clearSpecificIndexes(IndexService index, Class objectClass,
- IsModelObject rodaObject) throws AuthorizationDeniedException {
- if (AIP.class.equals(objectClass)) {
- List ids = Arrays.asList(rodaObject.getId());
- index.delete(IndexedRepresentation.class,
- new Filter(new OneOfManyFilterParameter(RodaConstants.REPRESENTATION_AIP_ID, ids)));
- index.delete(IndexedFile.class, new Filter(new OneOfManyFilterParameter(RodaConstants.FILE_AIP_ID, ids)));
- index.delete(IndexedPreservationEvent.class,
- new Filter(new OneOfManyFilterParameter(RodaConstants.PRESERVATION_EVENT_AIP_ID, ids)));
- }
- }
-
@Override
public void exportAll(StorageService toStorage) {
// TODO
}
@Override
- public void importObject(IsRODAObject object, StorageService fromStorage) {
- // TODO
+ public void importObject(ModelService fromModel, LiteRODAObject object, boolean replaceExisting)
+ throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
+ AlreadyExistsException {
+ StoragePath toObjectPath = ModelUtils.getStoragePath(object);
+ boolean existsBeforeImport = getStorage().exists(toObjectPath);
+
+ getStorage().importObject(fromModel.getStorage(), object, toObjectPath, replaceExisting);
+
+ boolean notifyUpdate = existsBeforeImport && replaceExisting;
+
+ if (notifyUpdate) {
+ notifyLiteRodaObjectUpdated(object);
+ } else {
+ notifyLiteRodaObjectCreated(object);
+ }
}
@Override
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 0d701343d3..14c4e31a3d 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
@@ -338,6 +338,19 @@ public AIP updateAIP(AIP aip, String updatedBy)
}
}
+ @Override
+ public AIP updateAIPOnHoldStatus(AIP aip, boolean status) throws AuthorizationDeniedException, GenericException {
+ TransactionalModelOperationLog operationLog = operationRegistry.registerUpdateOperationForAIP(aip.getId());
+ try {
+ AIP ret = getModelService().updateAIPOnHoldStatus(aip, status);
+ operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
+ return ret;
+ } catch (GenericException | AuthorizationDeniedException e) {
+ operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
+ throw e;
+ }
+ }
+
@Override
public AIP updateAIPState(AIP aip, String updatedBy)
throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException {
@@ -2288,6 +2301,10 @@ public BinaryVersion revertRiskVersion(String riskId, String versionId, Map directoryPath, String fileId,
ContentPayload contentPayload) throws RequestNotValidException, GenericException, AlreadyExistsException,
@@ -3239,14 +3270,15 @@ public DisposalConfirmation retrieveDisposalConfirmation(String disposalConfirma
}
@Override
- public void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold)
- throws GenericException, RequestNotValidException {
+ public void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold) throws GenericException,
+ RequestNotValidException, AuthorizationDeniedException, NotFoundException, AlreadyExistsException {
TransactionalModelOperationLog operationLog = operationRegistry
.registerOperationForDisposalConfirmation(disposalConfirmationId, OperationType.READ);
try {
getModelService().addDisposalHoldEntry(disposalConfirmationId, disposalHold);
operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
- } catch (GenericException | RequestNotValidException e) {
+ } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException
+ | AlreadyExistsException e) {
operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
throw e;
}
@@ -3254,13 +3286,15 @@ public void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold dis
@Override
public void addDisposalHoldTransitiveEntry(String disposalConfirmationId, DisposalHold transitiveDisposalHold)
- throws RequestNotValidException, GenericException {
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException {
TransactionalModelOperationLog operationLog = operationRegistry
.registerOperationForDisposalConfirmation(disposalConfirmationId, OperationType.READ);
try {
getModelService().addDisposalHoldTransitiveEntry(disposalConfirmationId, transitiveDisposalHold);
operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
- } catch (RequestNotValidException | GenericException e) {
+ } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException
+ | AlreadyExistsException e) {
operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
throw e;
}
@@ -3268,13 +3302,15 @@ public void addDisposalHoldTransitiveEntry(String disposalConfirmationId, Dispos
@Override
public void addDisposalScheduleEntry(String disposalConfirmationId, DisposalSchedule disposalSchedule)
- throws RequestNotValidException, GenericException {
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException {
TransactionalModelOperationLog operationLog = operationRegistry
.registerOperationForDisposalConfirmation(disposalConfirmationId, OperationType.UPDATE);
try {
getModelService().addDisposalScheduleEntry(disposalConfirmationId, disposalSchedule);
operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
- } catch (RequestNotValidException | GenericException e) {
+ } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException
+ | AlreadyExistsException e) {
operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
throw e;
}
@@ -3282,13 +3318,15 @@ public void addDisposalScheduleEntry(String disposalConfirmationId, DisposalSche
@Override
public void addAIPEntry(String disposalConfirmationId, DisposalConfirmationAIPEntry entry)
- throws RequestNotValidException, GenericException {
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, AlreadyExistsException,
+ NotFoundException {
TransactionalModelOperationLog operationLog = operationRegistry
.registerOperationForDisposalConfirmation(disposalConfirmationId, OperationType.UPDATE);
try {
getModelService().addAIPEntry(disposalConfirmationId, entry);
operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
- } catch (RequestNotValidException | GenericException e) {
+ } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException
+ | AlreadyExistsException e) {
operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
throw e;
}
@@ -3924,10 +3962,23 @@ public void exportAll(StorageService toStorage) {
}
@Override
- public void importObject(IsRODAObject object, StorageService fromStorage) {
- TransactionalModelOperationLog operationLog = operationRegistry.registerOperation(object, OperationType.UPDATE);
- getModelService().importObject(object, fromStorage);
- operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
+ public void importObject(ModelService fromModel, LiteRODAObject object, boolean replaceExisting)
+ throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
+ AlreadyExistsException {
+ TransactionalModelOperationLog operationLog;
+ if (replaceExisting) {
+ operationLog = operationRegistry.registerOperation(object.getInfo(), OperationType.CREATE_OR_UPDATE);
+ } else {
+ operationLog = operationRegistry.registerOperation(object.getInfo(), OperationType.CREATE);
+ }
+ try {
+ getModelService().importObject(fromModel, object, replaceExisting);
+ operationRegistry.updateOperationState(operationLog, OperationState.SUCCESS);
+ } catch (RequestNotValidException | NotFoundException | AuthorizationDeniedException | AlreadyExistsException
+ | GenericException e) {
+ operationRegistry.updateOperationState(operationLog, OperationState.FAILURE);
+ throw e;
+ }
}
@Override
@@ -4166,6 +4217,11 @@ public ReturnWithExceptionsWrapper notifyAipUpdatedOnChanged(AIP aip) {
return getModelService().notifyAipUpdatedOnChanged(aip);
}
+ @Override
+ public ReturnWithExceptionsWrapper notifyAipOnHoldStatusUpdated(AIP aip, boolean status) {
+ return getModelService().notifyAipOnHoldStatusUpdated(aip, status);
+ }
+
@Override
public ReturnWithExceptionsWrapper notifyAipDestroyed(AIP aip) {
return getModelService().notifyAipDestroyed(aip);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObservable.java b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObservable.java
index 0a00279aac..bbfb2f6823 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObservable.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObservable.java
@@ -44,6 +44,8 @@ public interface ModelObservable {
ReturnWithExceptionsWrapper notifyAipUpdatedOnChanged(AIP aip);
+ ReturnWithExceptionsWrapper notifyAipOnHoldStatusUpdated(AIP aip, boolean status);
+
ReturnWithExceptionsWrapper notifyAipDestroyed(AIP aip);
ReturnWithExceptionsWrapper notifyAipMoved(AIP aip, String oldParentId, String newParentId);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObserver.java b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObserver.java
index 817f981234..bdd62307e3 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObserver.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/model/ModelObserver.java
@@ -10,6 +10,7 @@
import java.util.List;
import org.roda.core.data.exceptions.ReturnWithExceptions;
+import org.roda.core.data.v2.LiteRODAObject;
import org.roda.core.data.v2.disposal.confirmation.DisposalConfirmation;
import org.roda.core.data.v2.ip.AIP;
import org.roda.core.data.v2.ip.DIP;
@@ -38,6 +39,8 @@ public interface ModelObserver {
public ReturnWithExceptions aipUpdatedOn(AIP aip);
+ ReturnWithExceptions aipOnHoldStatusUpdated(AIP aip, boolean status);
+
public ReturnWithExceptions aipDestroyed(AIP aip);
public ReturnWithExceptions aipStateUpdated(AIP aip);
@@ -151,4 +154,10 @@ public ReturnWithExceptions disposalConfirmationCreateOrUpd
DisposalConfirmation confirmation);
public ReturnWithExceptions disposalConfirmationDeleted(String confirmationId, boolean commit);
+
+ public ReturnWithExceptions liteRODAObjectCreated(LiteRODAObject liteRODAObject);
+
+ public ReturnWithExceptions liteRODAObjectUpdated(LiteRODAObject liteRODAObject);
+
+ public ReturnWithExceptions liteRODAObjectDeleted(LiteRODAObject liteRODAObject);
}
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 40f7ad8ab8..02c1ae5ce0 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
@@ -143,6 +143,8 @@ AIP destroyAIP(AIP aip, String updatedBy)
AIP updateAIP(AIP aip, String updatedBy)
throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException;
+ AIP updateAIPOnHoldStatus(AIP aip, boolean status) throws AuthorizationDeniedException, GenericException;
+
AIP updateAIPState(AIP aip, String updatedBy)
throws GenericException, NotFoundException, RequestNotValidException, AuthorizationDeniedException;
@@ -164,8 +166,8 @@ 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 ;
+ 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;
@@ -384,7 +386,7 @@ 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;
@@ -703,6 +705,9 @@ void createSubmission(StorageService submissionStorage, StoragePath submissionSt
void createSubmission(Path submissionPath, String aipId) throws AlreadyExistsException, GenericException,
RequestNotValidException, NotFoundException, AuthorizationDeniedException;
+ void createMetsFile(String aipId, String repId, ContentPayload contentPayload) throws RequestNotValidException,
+ GenericException, AlreadyExistsException, AuthorizationDeniedException, NotFoundException;
+
File createDocumentation(String aipId, String representationId, List directoryPath, String fileId,
ContentPayload contentPayload) throws RequestNotValidException, GenericException, AlreadyExistsException,
AuthorizationDeniedException, NotFoundException;
@@ -817,17 +822,19 @@ void deleteDisposalSchedule(String disposalScheduleId) throws NotFoundException,
DisposalConfirmation retrieveDisposalConfirmation(String disposalConfirmationId)
throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException;
- void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold)
- throws GenericException, RequestNotValidException;
+ void addDisposalHoldEntry(String disposalConfirmationId, DisposalHold disposalHold) throws GenericException,
+ RequestNotValidException, AuthorizationDeniedException, NotFoundException, AlreadyExistsException;
void addDisposalHoldTransitiveEntry(String disposalConfirmationId, DisposalHold transitiveDisposalHold)
- throws RequestNotValidException, GenericException;
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException;
void addDisposalScheduleEntry(String disposalConfirmationId, DisposalSchedule disposalSchedule)
- throws RequestNotValidException, GenericException;
+ throws RequestNotValidException, GenericException, AuthorizationDeniedException, NotFoundException,
+ AlreadyExistsException;
- void addAIPEntry(String disposalConfirmationId, DisposalConfirmationAIPEntry entry)
- throws RequestNotValidException, GenericException;
+ void addAIPEntry(String disposalConfirmationId, DisposalConfirmationAIPEntry entry) throws RequestNotValidException,
+ GenericException, AuthorizationDeniedException, NotFoundException, AlreadyExistsException;
DisposalConfirmation updateDisposalConfirmation(DisposalConfirmation disposalConfirmation)
throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException;
@@ -973,7 +980,9 @@ int importAll(IndexService index, final FileStorageService fromStorage, final bo
void exportAll(StorageService toStorage);
- void importObject(IsRODAObject object, StorageService fromStorage);
+ void importObject(ModelService fromModel, LiteRODAObject object, boolean replaceExisting)
+ throws RequestNotValidException, GenericException, NotFoundException, AuthorizationDeniedException,
+ AlreadyExistsException;
void exportObject(IsRODAObject object, StorageService toStorage, String... toPathPartials)
throws RequestNotValidException, AuthorizationDeniedException, AlreadyExistsException, NotFoundException,
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ModelUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ModelUtils.java
index fe87371ec8..06652ca493 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ModelUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ModelUtils.java
@@ -64,7 +64,6 @@
import org.roda.core.data.v2.risks.Risk;
import org.roda.core.data.v2.risks.RiskIncidence;
import org.roda.core.data.v2.synchronization.central.DistributedInstance;
-import org.roda.core.entity.transaction.TransactionalModelOperationLog;
import org.roda.core.index.IndexService;
import org.roda.core.model.lites.ParsedAIPLite;
import org.roda.core.model.lites.ParsedDIPFileLite;
@@ -167,6 +166,19 @@ public static StoragePath getRepresentationStoragePath(String aipId, String repr
return DefaultStoragePath.parse(getRepresentationPath(aipId, representationId));
}
+ public static StoragePath getMetsStoragePath(String aipId, String representationId) throws RequestNotValidException {
+
+ DefaultStoragePath metsOutputPath = null;
+
+ if (representationId != null) {
+ metsOutputPath = DefaultStoragePath.parse(build(getAIPPath(aipId),
+ RodaConstants.STORAGE_DIRECTORY_REPRESENTATIONS, representationId, RodaConstants.STORAGE_METS_FILENAME));
+ } else {
+ metsOutputPath = DefaultStoragePath.parse(build(getAIPPath(aipId), RodaConstants.STORAGE_METS_FILENAME));
+ }
+ return metsOutputPath;
+ }
+
private static List getRepresentationMetadataPath(String aipId, String representationId) {
return build(getRepresentationPath(aipId, representationId), RodaConstants.STORAGE_DIRECTORY_METADATA);
}
@@ -332,7 +344,10 @@ public static StoragePath getFileStoragePath(String aipId, String representation
}
if (StringUtils.isNotBlank(fileId)) {
path.add(fileId);
+ } else {
+ throw new RequestNotValidException("File ID cannot be null or blank");
}
+
return DefaultStoragePath.parse(path);
}
@@ -647,7 +662,7 @@ public static StoragePath getPreservationMetadataStoragePath(String id, Preserva
}
public static StoragePath getTechnicalMetadataStoragePath(String aipId, String representationId,
- List fileDirectoryPath, String fileId) throws RequestNotValidException {
+ List fileDirectoryPath, String fileId) throws RequestNotValidException {
List path = build(getRepresentationPath(aipId, representationId), RodaConstants.STORAGE_DIRECTORY_METADATA,
RodaConstants.STORAGE_DIRECTORY_TECHNICAL);
path.addAll(fileDirectoryPath);
@@ -1173,6 +1188,30 @@ public static StoragePath getDisposalConfirmationStoragePath(String disposalConf
return DefaultStoragePath.parse(getDisposalConfirmationPath(disposalConfirmationId));
}
+ public static StoragePath getDisposalSchedulesFromDisposalConfirmationStoragePath(String disposalConfirmationId)
+ throws RequestNotValidException {
+ return DefaultStoragePath.parse(getDisposalConfirmationStoragePath(disposalConfirmationId),
+ RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_SCHEDULES_FILENAME);
+ }
+
+ public static StoragePath getDisposalTransitiveHoldsFromDisposalConfirmationStoragePath(String disposalConfirmationId)
+ throws RequestNotValidException {
+ return DefaultStoragePath.parse(getDisposalConfirmationStoragePath(disposalConfirmationId),
+ RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_TRANSITIVE_HOLDS_FILENAME);
+ }
+
+ public static StoragePath getDisposalHoldsFromDisposalConfirmationStoragePath(String disposalConfirmationId)
+ throws RequestNotValidException {
+ return DefaultStoragePath.parse(getDisposalConfirmationStoragePath(disposalConfirmationId),
+ RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_HOLDS_FILENAME);
+ }
+
+ public static StoragePath getDisposalAipsFromDisposalConfirmationStoragePath(String disposalConfirmationId)
+ throws RequestNotValidException {
+ return DefaultStoragePath.parse(getDisposalConfirmationStoragePath(disposalConfirmationId),
+ RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_AIPS_FILENAME);
+ }
+
public static StoragePath getDisposalConfirmationAIPsPath(String disposalConfirmationId)
throws RequestNotValidException {
return DefaultStoragePath.parse(ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId),
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ResourceParseUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ResourceParseUtils.java
index 55dd3a31d3..4efe7bf847 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ResourceParseUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/model/utils/ResourceParseUtils.java
@@ -200,6 +200,8 @@ private static PreservationMetadata convertResourceToPreservationMetadata(Resour
type = PreservationMetadataType.FILE;
id = filename.substring(0, filename.length() - RodaConstants.PREMIS_SUFFIX.length());
fileDirectoryPath = ModelUtils.extractFilePathFromRepresentationPreservationMetadata(resourcePath);
+ String fileIdFromURN = URNUtils.getFileIdFromURN(filename);
+ fileId = fileIdFromURN.substring(0, fileIdFromURN.length() - RodaConstants.PREMIS_SUFFIX.length());
} else if (filename.endsWith(RodaConstants.OTHER_TECH_METADATA_FILE_SUFFIX)) {
type = PreservationMetadataType.OTHER;
fileDirectoryPath = ModelUtils.extractFilePathFromRepresentationPreservationMetadata(resourcePath);
@@ -475,7 +477,7 @@ private static CloseableIterabl
if (isDirectoryAcceptable(classToReturn)) {
filtered = iterable;
} else {
- filtered = CloseableIterables.filter(iterable, p -> !p.isDirectory());
+ filtered = CloseableIterables.filter(iterable, p -> p != null && !p.isDirectory());
}
CloseableIterable> it;
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/antivirus/ClamAntiVirus.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/antivirus/ClamAntiVirus.java
index 2e3bef7e95..1a0161e341 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/antivirus/ClamAntiVirus.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/antivirus/ClamAntiVirus.java
@@ -136,10 +136,10 @@ public VirusCheckResult checkForVirus(Path path) throws RuntimeException {
LOGGER.debug("Executing virus scan in {}", path);
- String clamavBin = RodaCoreFactory.getRodaConfiguration()
- .getString("core.plugins.internal.virus_check.clamav.bin", "clamscan");
- String clamavParams = RodaCoreFactory.getRodaConfiguration()
- .getString("core.plugins.internal.virus_check.clamav.params", "-ri");
+ String clamavBin = RodaCoreFactory.getConfigurationManager()
+ .getConfigurationString("core.plugins.internal.virus_check.clamav.bin", "clamscan");
+ String clamavParams = RodaCoreFactory.getConfigurationManager()
+ .getConfigurationString("core.plugins.internal.virus_check.clamav.params", "-ri");
List command = new ArrayList<>();
command.add(clamavBin);
@@ -160,8 +160,8 @@ public VirusCheckResult checkForVirus(Path path) throws RuntimeException {
@Override
public String getVersion() {
- String clamavGetVersion = RodaCoreFactory.getRodaConfiguration()
- .getString("core.plugins.internal.virus_check.clamav.get_version", "clamscan --version");
+ String clamavGetVersion = RodaCoreFactory.getConfigurationManager()
+ .getConfigurationString("core.plugins.internal.virus_check.clamav.get_version", "clamscan --version");
List command = new ArrayList<>(Arrays.asList(clamavGetVersion.split(" ")));
try {
String executeOutput = CommandUtility.execute(command, false);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/characterization/SiegfriedPluginUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/characterization/SiegfriedPluginUtils.java
index 415f0e7ca5..0ef7ffc62c 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/characterization/SiegfriedPluginUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/characterization/SiegfriedPluginUtils.java
@@ -72,14 +72,14 @@ private SiegfriedPluginUtils() {
private static List getBatchCommand(Path sourceDirectory) {
List command;
- String siegfriedPath = RodaCoreFactory.getRodaConfiguration().getString("core.tools.siegfried.binary", "sf");
+ String siegfriedPath = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.tools.siegfried.binary", "sf");
command = new ArrayList<>(
Arrays.asList(siegfriedPath, "-json=true", "-z=false", sourceDirectory.toFile().getAbsolutePath()));
return command;
}
private static String getSiegfriedServerEndpoint(Path sourceDirectory) {
- String siegfriedServer = RodaCoreFactory.getRodaConfiguration().getString("core.tools.siegfried.server",
+ String siegfriedServer = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.tools.siegfried.server",
"http://localhost:5138");
return String.format("%s/identify/%s?base64=true&format=json", siegfriedServer,
@@ -88,7 +88,7 @@ private static String getSiegfriedServerEndpoint(Path sourceDirectory) {
public static String runSiegfriedOnPath(Path sourceDirectory) throws PluginException {
try {
- String siegfriedMode = RodaCoreFactory.getRodaConfiguration().getString("core.tools.siegfried.mode", "server");
+ String siegfriedMode = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.tools.siegfried.mode", "server");
if ("server".equalsIgnoreCase(siegfriedMode)) {
LOGGER.debug("Running Siegfried on server mode");
String endpoint = getSiegfriedServerEndpoint(sourceDirectory);
@@ -106,7 +106,7 @@ public static String runSiegfriedOnPath(Path sourceDirectory) throws PluginExcep
public static String getVersion() {
String version = null;
- String siegfriedMode = RodaCoreFactory.getRodaConfiguration().getString("core.tools.siegfried.mode", "server");
+ String siegfriedMode = RodaCoreFactory.getConfigurationManager().getConfigurationString("core.tools.siegfried.mode", "server");
if ("server".equalsIgnoreCase(siegfriedMode)) {
LOGGER.debug("Running Siegfried on server mode");
String endpoint = getSiegfriedServerEndpoint(Paths.get("/dev/null"));
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/CreateDisposalConfirmationPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/CreateDisposalConfirmationPlugin.java
index 5a62f23e33..6a5b7dba79 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/CreateDisposalConfirmationPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/CreateDisposalConfirmationPlugin.java
@@ -12,6 +12,7 @@
import static org.roda.core.data.common.RodaConstants.PLUGIN_PARAMS_DISPOSAL_CONFIRMATION_TITLE;
import static org.roda.core.data.common.RodaConstants.PreservationEventType;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -259,7 +260,8 @@ private void processAIP(ModelService model, IndexService index, Report report, J
"was successfully assign to disposal confirmation", confirmationId, aip.getId());
aipCounter++;
- } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException e) {
+ } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException |
+ AlreadyExistsException e) {
LOGGER.error("Failed to assign AIP '{}' to disposal confirmation '{}': {}", aip.getId(), confirmationId,
e.getMessage(), e);
state = PluginState.FAILURE;
@@ -294,33 +296,32 @@ private void processAIP(ModelService model, IndexService index, Report report, J
model.addDisposalScheduleEntry(confirmationId, disposalSchedule);
}
}
- } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException e) {
+ } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException
+ | AlreadyExistsException e) {
LOGGER.error("Failed to create disposal schedules jsonl file", e);
report.addPluginDetails("Failed to create jsonl with disposal schedules");
}
// Make disposal holds as a jsonl
try {
- FSUtils.createFile(DisposalConfirmationPluginUtils.getDisposalConfirmationPath(confirmationId),
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_HOLDS_FILENAME, true, true);
for (String disposalHoldId : disposalHolds) {
DisposalHold disposalHold = model.retrieveDisposalHold(disposalHoldId);
model.addDisposalHoldEntry(confirmationId, disposalHold);
}
- } catch (NotFoundException | AuthorizationDeniedException | GenericException | RequestNotValidException e) {
+ } catch (NotFoundException | AuthorizationDeniedException | GenericException | RequestNotValidException
+ | AlreadyExistsException e) {
LOGGER.error("Failed to create disposal holds jsonl file", e);
report.addPluginDetails("Failed to create jsonl with disposal holds");
}
// Make disposal holds transitive as a jsonl
try {
- FSUtils.createFile(DisposalConfirmationPluginUtils.getDisposalConfirmationPath(confirmationId),
- RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_TRANSITIVE_HOLDS_FILENAME, true, true);
for (String disposalHoldId : disposalHoldTransitives) {
DisposalHold disposalHold = model.retrieveDisposalHold(disposalHoldId);
model.addDisposalHoldTransitiveEntry(confirmationId, disposalHold);
}
- } catch (NotFoundException | AuthorizationDeniedException | GenericException | RequestNotValidException e) {
+ } catch (NotFoundException | AuthorizationDeniedException | GenericException | RequestNotValidException
+ | AlreadyExistsException e) {
LOGGER.error("Failed to create transitive disposal holds jsonl file", e);
report.addPluginDetails("Failed to create jsonl with transitive disposal holds");
}
@@ -409,7 +410,8 @@ private void processChild(IndexedAIP child, String topAncestorId, String confirm
"was skipped from being assign to disposal confirmation due to incompatible disposal schedule",
confirmationId, aip.getId());
}
- } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException e) {
+ } catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException |
+ AlreadyExistsException e) {
LOGGER.error("Failed to assign AIP '{}' to disposal confirmation '{}': {}", child.getId(), confirmationId,
e.getMessage(), e);
state = PluginState.FAILURE;
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DeleteDisposalConfirmationPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DeleteDisposalConfirmationPlugin.java
index 92fc2715fc..6317f0a75c 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DeleteDisposalConfirmationPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DeleteDisposalConfirmationPlugin.java
@@ -10,6 +10,7 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -159,9 +160,9 @@ private void processDisposalConfirmation(ModelService model, Report report, JobP
Binary binary = model.getBinary(confirmation,
RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_AIPS_FILENAME);
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(binary.getContent().createInputStream()))) {
- while (reader.ready()) {
- String aipEntryJson = reader.readLine();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(binary.getContent().createInputStream(), StandardCharsets.UTF_8))) {
+ String aipEntryJson;
+ while ((aipEntryJson = reader.readLine()) != null) {
DisposalConfirmationAIPEntry aipEntry = JsonUtils.getObjectFromJson(aipEntryJson,
DisposalConfirmationAIPEntry.class);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DestroyRecordsPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DestroyRecordsPlugin.java
index 23b7a856ad..c203fff076 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DestroyRecordsPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DestroyRecordsPlugin.java
@@ -12,13 +12,17 @@
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.UUID;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream;
+import org.roda.core.RodaCoreFactory;
import org.roda.core.common.RodaUtils;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.exceptions.AlreadyExistsException;
@@ -36,12 +40,16 @@
import org.roda.core.data.v2.ip.AIPState;
import org.roda.core.data.v2.ip.Representation;
import org.roda.core.data.v2.ip.metadata.DescriptiveMetadata;
+import org.roda.core.data.v2.ip.metadata.PreservationMetadata;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.PluginState;
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.index.IndexService;
+import org.roda.core.model.DefaultModelService;
+import org.roda.core.model.LiteRODAObjectFactory;
import org.roda.core.model.ModelService;
+import org.roda.core.model.utils.ModelUtils;
import org.roda.core.plugins.AbstractPlugin;
import org.roda.core.plugins.Plugin;
import org.roda.core.plugins.PluginException;
@@ -49,7 +57,11 @@
import org.roda.core.plugins.RODAObjectProcessingLogic;
import org.roda.core.plugins.orchestrate.JobPluginInfo;
import org.roda.core.storage.Binary;
+import org.roda.core.storage.DefaultStoragePath;
+import org.roda.core.storage.StorageService;
+import org.roda.core.storage.StorageServiceUtils;
import org.roda.core.storage.StringContentPayload;
+import org.roda.core.storage.fs.FileStorageService;
import org.roda.core.util.CommandException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -150,12 +162,27 @@ private void processDisposalConfirmation(ModelService model, Report report, Job
Binary binary = model.getBinary(disposalConfirmation,
RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_AIPS_FILENAME);
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(binary.getContent().createInputStream()))) {
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(binary.getContent().createInputStream(), StandardCharsets.UTF_8))) {
jobPluginInfo.setSourceObjectsCount(disposalConfirmation.getNumberOfAIPs().intValue());
+
+ StorageService disposalBinStorage = new FileStorageService(
+ RodaCoreFactory.getDisposalBinDirectoryPath().resolve(disposalConfirmation.getId()), false, null, false);
+
+ if (disposalBinStorage.countResourcesUnderContainer(DefaultStoragePath.empty(), false) > 0) {
+ throw new RequestNotValidException("Disposal bin structure for disposal confirmation '"
+ + disposalConfirmation.getTitle() + "' (" + disposalConfirmationId
+ + ") already exists in storage. Please check the disposal bin and remove the existing structure before executing the plugin.");
+ }
+
+ ModelService disposalBinModel = new DefaultModelService(disposalBinStorage, null,
+ RodaConstants.NodeType.REPLICA, UUID.randomUUID().toString());
+
// Iterate over the AIP
- while (reader.ready()) {
- String aipEntryJson = reader.readLine();
- processAipEntry(aipEntryJson, disposalConfirmation, model, cachedJob, report, jobPluginInfo);
+ String aipEntryJson;
+ while ((aipEntryJson = reader.readLine()) != null) {
+ processAipEntry(aipEntryJson, disposalConfirmation, model, cachedJob, report, jobPluginInfo,
+ disposalBinModel);
}
}
} catch (GenericException | NotFoundException | RequestNotValidException | AuthorizationDeniedException
@@ -190,12 +217,12 @@ private void processDisposalConfirmation(ModelService model, Report report, Job
}
private void processAipEntry(String aipEntryJson, DisposalConfirmation disposalConfirmation, ModelService model,
- Job cachedJob, Report report, JobPluginInfo jobPluginInfo) {
+ Job cachedJob, Report report, JobPluginInfo jobPluginInfo, ModelService disposalBinModel) {
try {
DisposalConfirmationAIPEntry aipEntry = JsonUtils.getObjectFromJson(aipEntryJson,
DisposalConfirmationAIPEntry.class);
AIP aip = model.retrieveAIP(aipEntry.getAipId());
- processAIP(aip, disposalConfirmation, model, cachedJob, report, jobPluginInfo);
+ processAIP(aip, disposalConfirmation, model, cachedJob, report, jobPluginInfo, disposalBinModel);
} catch (GenericException | NotFoundException | RequestNotValidException | AuthorizationDeniedException e) {
LOGGER.error("Failed to process AIP entry '{}': {}", aipEntryJson, e.getMessage(), e);
processedWithErrors = true;
@@ -211,7 +238,7 @@ private void processAipEntry(String aipEntryJson, DisposalConfirmation disposalC
}
private void processAIP(AIP aip, DisposalConfirmation disposalConfirmation, ModelService model, Job cachedJob,
- Report report, JobPluginInfo jobPluginInfo) {
+ Report report, JobPluginInfo jobPluginInfo, ModelService disposalBinModel) {
LOGGER.debug("Processing AIP {}", aip.getId());
@@ -229,7 +256,7 @@ private void processAIP(AIP aip, DisposalConfirmation disposalConfirmation, Mode
aip.setState(AIPState.DESTROY_PROCESSING);
model.updateAIPState(aip, cachedJob.getUsername());
- testAndExecuteCopyAIP2DisposalBin(aip, disposalConfirmation.getId());
+ disposalBinModel.importObject(model, LiteRODAObjectFactory.get(AIP.class, aip.getId()).orElseThrow(), false);
executeSetAIPMetadataInformation(aip, cachedJob.getUsername());
@@ -245,7 +272,7 @@ private void processAIP(AIP aip, DisposalConfirmation disposalConfirmation, Mode
}
reportItem.setPluginDetails(outcomeText);
- } catch (IOException | CommandException | RequestNotValidException | GenericException | AuthorizationDeniedException
+ } catch (IOException | RequestNotValidException | GenericException | AuthorizationDeniedException
| NotFoundException | AlreadyExistsException e) {
LOGGER.error("Failed to destroy AIP '{}': {}", aip.getId(), e.getMessage(), e);
state = PluginState.FAILURE;
@@ -255,17 +282,16 @@ private void processAIP(AIP aip, DisposalConfirmation disposalConfirmation, Mode
processedWithErrors = true;
}
- model.createEvent(aip.getId(), null, null, null, RodaConstants.PreservationEventType.DESTRUCTION, EVENT_DESCRIPTION,
- null, null, state, outcomeText, "", cachedJob.getUsername(), true, null);
+ PreservationMetadata event = model.createEvent(aip.getId(), null, null, null,
+ RodaConstants.PreservationEventType.DESTRUCTION, EVENT_DESCRIPTION, null, null, state, outcomeText, "",
+ cachedJob.getUsername(), true, null);
// copy the preservation event to the AIP in the disposal bin
- // using the --ignore-existing flag in the rsync process, copying only the new
- // preservation event, leaving the remaining AIP structure intact
try {
- DisposalConfirmationPluginUtils.copyAIPToDisposalBin(aip, disposalConfirmation.getId(),
- Arrays.asList("-r", "--ignore-existing"));
- } catch (RequestNotValidException | GenericException | CommandException e) {
- LOGGER.error("Failed to copy preservation event: {}", e.getMessage(), e);
+ disposalBinModel.importObject(model, LiteRODAObjectFactory.get(event).orElseThrow(), false);
+ } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException
+ | AlreadyExistsException e) {
+ throw new RuntimeException(e);
}
jobPluginInfo.incrementObjectsProcessed(state);
@@ -285,16 +311,6 @@ private void executeRemoveAllRepresentations(AIP aip, ModelService model, String
aip.getRepresentations().clear();
}
- private void testAndExecuteCopyAIP2DisposalBin(AIP aip, String disposalConfirmationId)
- throws GenericException, CommandException, RequestNotValidException {
- // test if the AIP was copied to disposal bin
- if (!DisposalConfirmationPluginUtils.aipExistsInDisposalBin(aip.getId(), disposalConfirmationId)) {
- // Copy AIP to disposal bin
- DisposalConfirmationPluginUtils.copyAIPToDisposalBin(aip, disposalConfirmationId,
- Collections.singletonList("-r"));
- }
- }
-
private void executeSetAIPMetadataInformation(AIP aip, String destructionBy) {
DisposalDestructionAIPMetadata destruction = aip.getDisposal().getConfirmation().getDestruction();
if (destruction == null) {
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DisposalConfirmationPluginUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DisposalConfirmationPluginUtils.java
index 8f2792168a..c4c2885694 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DisposalConfirmationPluginUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/DisposalConfirmationPluginUtils.java
@@ -15,7 +15,6 @@
import java.util.Set;
import java.util.stream.Collectors;
-import org.roda.core.RodaCoreFactory;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
@@ -30,14 +29,8 @@
import org.roda.core.data.v2.ip.AIP;
import org.roda.core.data.v2.ip.IndexedAIP;
import org.roda.core.data.v2.ip.IndexedRepresentation;
-import org.roda.core.data.v2.ip.StoragePath;
import org.roda.core.index.IndexService;
import org.roda.core.index.utils.IterableIndexResult;
-import org.roda.core.model.utils.ModelUtils;
-import org.roda.core.storage.DefaultStoragePath;
-import org.roda.core.storage.fs.FSUtils;
-import org.roda.core.storage.rsync.RsyncUtils;
-import org.roda.core.util.CommandException;
/**
* @author Miguel Guimarães
@@ -47,38 +40,6 @@ public class DisposalConfirmationPluginUtils {
private DisposalConfirmationPluginUtils() {
}
- public static boolean aipExistsInDisposalBin(String aipId, String disposalConfirmationId) {
- // disposal-bin//aip/
- Path disposalBinPath = RodaCoreFactory.getDisposalBinDirectoryPath().resolve(disposalConfirmationId)
- .resolve(RodaConstants.CORE_AIP_FOLDER).resolve(aipId);
-
- return FSUtils.exists(disposalBinPath);
- }
-
- public static void copyAIPToDisposalBin(AIP aip, String disposalConfirmationId, List rsyncOptions)
- throws RequestNotValidException, GenericException, CommandException {
- StoragePath aipStoragePath = ModelUtils.getAIPStoragePath(aip.getId());
- Path aipPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), aipStoragePath);
-
- // disposal-bin//aip/
- Path disposalBinPath = RodaCoreFactory.getDisposalBinDirectoryPath().resolve(disposalConfirmationId)
- .resolve(RodaConstants.CORE_AIP_FOLDER).resolve(aipStoragePath.getName());
-
- RsyncUtils.executeRsync(aipPath, disposalBinPath, rsyncOptions);
- }
-
- public static void copyAIPFromDisposalBin(String aipId, String disposalConfirmationId, List rsyncOptions)
- throws RequestNotValidException, GenericException, CommandException {
- StoragePath aipStoragePath = ModelUtils.getAIPStoragePath(aipId);
- Path aipPath = FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), aipStoragePath);
-
- // disposal-bin//aip/
- Path disposalBinPath = RodaCoreFactory.getDisposalBinDirectoryPath().resolve(disposalConfirmationId)
- .resolve(RodaConstants.CORE_AIP_FOLDER).resolve(aipStoragePath.getName());
-
- RsyncUtils.executeRsync(disposalBinPath, aipPath, rsyncOptions);
- }
-
public static DisposalConfirmation getDisposalConfirmation(String confirmationId, String title, long storageSize,
Set disposalHolds, Set disposalSchedules, long numberOfAIPs, Map extraFields) {
@@ -177,10 +138,4 @@ private static void getStorageSizeInBytesForAIP(IndexService indexService, Strin
entry.setAipSize(totalSize);
entry.setAipNumberOfFiles(totalOfDataFiles);
}
-
- public static Path getDisposalConfirmationPath(String disposalConfirmationId) throws RequestNotValidException {
- DefaultStoragePath confirmationPath = DefaultStoragePath
- .parse(ModelUtils.getDisposalConfirmationStoragePath(disposalConfirmationId));
- return FSUtils.getEntityPath(RodaCoreFactory.getStoragePath(), confirmationPath);
- }
}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/PermanentlyDeleteRecordsPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/PermanentlyDeleteRecordsPlugin.java
index 1f4513b42d..b89e090964 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/PermanentlyDeleteRecordsPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/PermanentlyDeleteRecordsPlugin.java
@@ -25,6 +25,7 @@
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.index.IndexService;
+import org.roda.core.model.DefaultModelService;
import org.roda.core.model.ModelService;
import org.roda.core.plugins.AbstractPlugin;
import org.roda.core.plugins.Plugin;
@@ -32,7 +33,11 @@
import org.roda.core.plugins.PluginHelper;
import org.roda.core.plugins.RODAObjectProcessingLogic;
import org.roda.core.plugins.orchestrate.JobPluginInfo;
+import org.roda.core.storage.DefaultStoragePath;
+import org.roda.core.storage.DirectResourceAccess;
+import org.roda.core.storage.StorageService;
import org.roda.core.storage.fs.FSUtils;
+import org.roda.core.storage.fs.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -116,15 +121,17 @@ private void processDisposalConfirmation(ModelService model, Report report, Job
PluginHelper.updatePartialJobReport(this, model, reportItem, false, cachedJob);
PluginState state = PluginState.SUCCESS;
try {
- // disposal-bin//*
- Path disposalBinPath = RodaCoreFactory.getDisposalBinDirectoryPath().resolve(confirmation.getId());
- FSUtils.deletePath(disposalBinPath);
-
confirmation.setState(DisposalConfirmationState.PERMANENTLY_DELETED);
model.updateDisposalConfirmation(confirmation);
reportItem.setPluginDetails("Records under disposal confirmation '" + confirmation.getTitle() + "' ("
+ confirmation.getId() + ") were deleted permanently");
+
+ StorageService disposalBinStorage = new FileStorageService(
+ RodaCoreFactory.getDisposalBinDirectoryPath().resolve(confirmation.getId()), false, null, false);
+
+ DirectResourceAccess directAccess = disposalBinStorage.getDirectAccess(DefaultStoragePath.empty());
+ FSUtils.deletePathQuietly(directAccess.getPath());
} catch (AuthorizationDeniedException | RequestNotValidException | NotFoundException | GenericException e) {
LOGGER.error("Failed to permanently delete the records under disposal confirmation '{}' ({}): {}",
confirmation.getTitle(), confirmation.getId(), e.getMessage(), e);
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/RestoreRecordsPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/RestoreRecordsPlugin.java
index 3b6e80d887..6ea7518ac6 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/RestoreRecordsPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/confirmation/RestoreRecordsPlugin.java
@@ -10,13 +10,16 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Date;
import java.util.List;
+import java.util.UUID;
import org.roda.core.RodaCoreFactory;
import org.roda.core.data.common.RodaConstants;
+import org.roda.core.data.exceptions.AlreadyExistsException;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
@@ -33,6 +36,9 @@
import org.roda.core.data.v2.jobs.PluginType;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.index.IndexService;
+import org.roda.core.model.DefaultModelService;
+import org.roda.core.model.DefaultTransactionalModelService;
+import org.roda.core.model.LiteRODAObjectFactory;
import org.roda.core.model.ModelService;
import org.roda.core.plugins.AbstractPlugin;
import org.roda.core.plugins.Plugin;
@@ -41,7 +47,9 @@
import org.roda.core.plugins.RODAObjectProcessingLogic;
import org.roda.core.plugins.orchestrate.JobPluginInfo;
import org.roda.core.storage.Binary;
+import org.roda.core.storage.StorageService;
import org.roda.core.storage.fs.FSUtils;
+import org.roda.core.storage.fs.FileStorageService;
import org.roda.core.util.CommandException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,24 +63,24 @@ public class RestoreRecordsPlugin extends AbstractPlugin {
private boolean processedWithErrors = false;
+ public static String getStaticName() {
+ return "Restore records under disposal confirmation report";
+ }
+
+ public static String getStaticDescription() {
+ return "";
+ }
+
@Override
public String getVersionImpl() {
return "1.0";
}
- public static String getStaticName() {
- return "Restore records under disposal confirmation report";
- }
-
@Override
public String getName() {
return getStaticName();
}
- public static String getStaticDescription() {
- return "";
- }
-
@Override
public String getDescription() {
return getStaticDescription();
@@ -126,13 +134,20 @@ private void processDisposalConfirmation(IndexService index, ModelService model,
Binary binary = model.getBinary(disposalConfirmation,
RodaConstants.STORAGE_DIRECTORY_DISPOSAL_CONFIRMATION_AIPS_FILENAME);
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(binary.getContent().createInputStream()))) {
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(binary.getContent().createInputStream(), StandardCharsets.UTF_8))) {
jobPluginInfo.setSourceObjectsCount(disposalConfirmation.getNumberOfAIPs().intValue());
- // Iterate over the AIP
- while (reader.ready()) {
- String aipEntryJson = reader.readLine();
+ StorageService disposalBinStorage = new FileStorageService(
+ RodaCoreFactory.getDisposalBinDirectoryPath().resolve(disposalConfirmation.getId()), false, null, false);
- processAipEntry(aipEntryJson, disposalConfirmation, index, model, cachedJob, report, jobPluginInfo);
+ ModelService disposalBinModel = new DefaultModelService(disposalBinStorage, null,
+ RodaConstants.NodeType.REPLICA, UUID.randomUUID().toString());
+
+ // Iterate over the AIP
+ String aipEntryJson;
+ while ((aipEntryJson = reader.readLine()) != null) {
+ processAipEntry(aipEntryJson, disposalConfirmation, index, model, cachedJob, report, jobPluginInfo,
+ disposalBinModel);
}
}
} catch (RequestNotValidException | AuthorizationDeniedException | GenericException | NotFoundException
@@ -173,11 +188,11 @@ private void processDisposalConfirmation(IndexService index, ModelService model,
}
private void processAipEntry(String aipEntryJson, DisposalConfirmation disposalConfirmation, IndexService index,
- ModelService model, Job cachedJob, Report report, JobPluginInfo jobPluginInfo) {
+ ModelService model, Job cachedJob, Report report, JobPluginInfo jobPluginInfo, ModelService disposalBinModel) {
try {
DisposalConfirmationAIPEntry aipEntry = JsonUtils.getObjectFromJson(aipEntryJson,
DisposalConfirmationAIPEntry.class);
- processAIP(aipEntry, disposalConfirmation, index, model, cachedJob, report, jobPluginInfo);
+ processAIP(aipEntry, disposalConfirmation, index, model, cachedJob, report, jobPluginInfo, disposalBinModel);
} catch (GenericException e) {
LOGGER.error("Failed to process the AIP entry '{}': {}", aipEntryJson, e.getMessage(), e);
processedWithErrors = true;
@@ -193,7 +208,8 @@ private void processAipEntry(String aipEntryJson, DisposalConfirmation disposalC
}
private void processAIP(DisposalConfirmationAIPEntry aipEntry, DisposalConfirmation disposalConfirmation,
- IndexService index, ModelService model, Job cachedJob, Report report, JobPluginInfo jobPluginInfo) {
+ IndexService index, ModelService model, Job cachedJob, Report report, JobPluginInfo jobPluginInfo,
+ ModelService disposalBinModel) {
LOGGER.debug("Processing AIP entry {}", aipEntry.getAipId());
@@ -205,8 +221,8 @@ private void processAIP(DisposalConfirmationAIPEntry aipEntry, DisposalConfirmat
try {
// Copy AIP from disposal bin to storage
- DisposalConfirmationPluginUtils.copyAIPFromDisposalBin(aipEntry.getAipId(), disposalConfirmation.getId(),
- Collections.singletonList("-r"));
+ model.importObject(disposalBinModel, LiteRODAObjectFactory.get(AIP.class, aipEntry.getAipId()).orElseThrow(),
+ true);
// reindex the AIP
AIP aip = model.retrieveAIP(aipEntry.getAipId());
@@ -223,8 +239,8 @@ private void processAIP(DisposalConfirmationAIPEntry aipEntry, DisposalConfirmat
reportItem.setPluginDetails(outcomeText);
- } catch (CommandException | RequestNotValidException | GenericException | NotFoundException
- | AuthorizationDeniedException e) {
+ } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException
+ | AlreadyExistsException e) {
LOGGER.error("Failed to restore AIP '{}': {}", aipEntry.getAipId(), e.getMessage(), e);
pluginState = PluginState.FAILURE;
outcomeText = "AIP '" + aipEntry.getAipId()
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/hold/LiftDisposalHoldPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/hold/LiftDisposalHoldPlugin.java
index c3c5c374cf..20d7d98840 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/hold/LiftDisposalHoldPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/disposal/hold/LiftDisposalHoldPlugin.java
@@ -55,10 +55,6 @@
*/
public class LiftDisposalHoldPlugin extends AbstractPlugin {
private static final Logger LOGGER = LoggerFactory.getLogger(LiftDisposalHoldPlugin.class);
-
- private String disposalHoldId;
- private String details;
-
private static final Map pluginParameters = new HashMap<>();
static {
@@ -74,6 +70,17 @@ public class LiftDisposalHoldPlugin extends AbstractPlugin {
.isMandatory(false).withDescription("Details that will be used when creating event").build());
}
+ private String disposalHoldId;
+ private String details;
+
+ public static String getStaticName() {
+ return "Lift disposal hold";
+ }
+
+ public static String getStaticDescription() {
+ return "";
+ }
+
@Override
public List getParameters() {
ArrayList parameters = new ArrayList<>();
@@ -100,19 +107,11 @@ public String getVersionImpl() {
return "1.0";
}
- public static String getStaticName() {
- return "Lift disposal hold";
- }
-
@Override
public String getName() {
return getStaticName();
}
- public static String getStaticDescription() {
- return "";
- }
-
@Override
public String getDescription() {
return getStaticDescription();
@@ -163,7 +162,28 @@ public void process(IndexService index, ModelService model, Report report, Job c
private void liftDisposalHold(IndexService index, ModelService model, Report report, Job cachedJob,
JobPluginInfo jobPluginInfo) {
report.addPluginDetails(details);
- int count = 0;
+ long count = 0;
+
+ try {
+ DisposalHold disposalHold = model.retrieveDisposalHold(disposalHoldId);
+ disposalHold.setState(DisposalHoldState.LIFTED);
+ disposalHold.setLiftedBy(cachedJob.getUsername());
+ disposalHold.setLiftedOn(new Date());
+ model.updateDisposalHold(disposalHold, cachedJob.getUsername(), details);
+ } catch (RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException
+ | IllegalOperationException e) {
+ Report reportItem = PluginHelper.initPluginReportItem(this, disposalHoldId, DisposalHold.class);
+ PluginHelper.updatePartialJobReport(this, model, reportItem, false, cachedJob);
+ PluginState state = PluginState.FAILURE;
+ jobPluginInfo.incrementObjectsProcessedWithFailure();
+ reportItem.setPluginState(state)
+ .setPluginDetails("Error lifting disposal hold '" + disposalHoldId + "': " + e.getMessage());
+ report.addReport(reportItem);
+ jobPluginInfo.setSourceObjectsCount(1);
+ PluginHelper.updatePartialJobReport(this, model, reportItem, true, cachedJob);
+
+ return;
+ }
try (IterableIndexResult aipsToDelete = findAipsWithDisposalHold(index, disposalHoldId)) {
for (IndexedAIP indexedAIP : aipsToDelete) {
@@ -175,18 +195,12 @@ private void liftDisposalHold(IndexService index, ModelService model, Report rep
try {
AIP aip = model.retrieveAIP(indexedAIP.getId());
- Pair outcome = DisposalHoldPluginUtils.disassociateDisposalHoldFromAIP(disposalHoldId, aip,
- reportItem);
- boolean lifted = outcome.getFirst();
- outcomeText = outcome.getSecond();
- processTransitiveAIP(model, index, cachedJob, aip.getId(), disposalHoldId, jobPluginInfo, report);
- model.updateAIP(aip, cachedJob.getUsername());
- if (lifted) {
- jobPluginInfo.incrementObjectsProcessedWithSuccess();
- } else {
- jobPluginInfo.incrementObjectsProcessedWithSkipped();
- }
+ long children = processTransitiveAIP(model, index, cachedJob, aip.getId(), disposalHoldId, jobPluginInfo, report);
+ model.updateAIPOnHoldStatus(aip, model.onDisposalHold(aip.getId()));
+ jobPluginInfo.incrementObjectsProcessedWithSuccess();
reportItem.setPluginState(state);
+ outcomeText = "";
+ count += children;
} catch (GenericException | NotFoundException | RequestNotValidException | AuthorizationDeniedException e) {
outcomeText = "Error lifting disposal hold" + disposalHoldId + " from AIP " + indexedAIP.getId();
LOGGER.error("Error lifting disposal hold '{}' from '{}': {}", disposalHoldId, indexedAIP.getId(),
@@ -210,16 +224,8 @@ private void liftDisposalHold(IndexService index, ModelService model, Report rep
count++;
}
- jobPluginInfo.setSourceObjectsCount(count);
-
- DisposalHold disposalHold = model.retrieveDisposalHold(disposalHoldId);
- disposalHold.setState(DisposalHoldState.LIFTED);
- disposalHold.setLiftedBy(cachedJob.getUsername());
- disposalHold.setLiftedOn(new Date());
- model.updateDisposalHold(disposalHold, cachedJob.getUsername(), details);
-
- } catch (IOException | GenericException | RequestNotValidException | NotFoundException
- | AuthorizationDeniedException | IllegalOperationException e) {
+ jobPluginInfo.setSourceObjectsCount((int) count);
+ } catch (IOException | GenericException | RequestNotValidException e) {
LOGGER.error("Error getting AIPs to delete", e);
}
}
@@ -231,7 +237,7 @@ private IterableIndexResult findAipsWithDisposalHold(IndexService in
return index.findAll(IndexedAIP.class, aipsFilter, false, List.of(RodaConstants.INDEX_UUID));
}
- private void processTransitiveAIP(ModelService model, IndexService index, Job cachedJob, String aipId, String holdId,
+ private Long processTransitiveAIP(ModelService model, IndexService index, Job cachedJob, String aipId, String holdId,
JobPluginInfo jobPluginInfo, Report report) throws GenericException, NotFoundException, RequestNotValidException {
IterableIndexResult results = DisposalHoldPluginUtils.getTransitivesHoldsAIPs(index, aipId);
@@ -245,12 +251,10 @@ private void processTransitiveAIP(ModelService model, IndexService index, Job ca
try {
AIP aipChildren = model.retrieveAIP(indexedAIP.getId());
LOGGER.debug("Processing transitive AIP {}", aipId);
- outcomeText = DisposalHoldPluginUtils.disassociateTransitiveDisposalHoldFromAIP(disposalHoldId, aipChildren, reportItem);
- model.updateAIP(aipChildren, cachedJob.getUsername());
- reportItem.setPluginState(state)
- .addPluginDetails("transitive Disposal hold '" + holdId + " was successfully lifting to AIP '" + aipId + "'");
+ model.updateAIPOnHoldStatus(aipChildren, model.onDisposalHold(aipChildren.getId()));
+ reportItem.setPluginState(state);
jobPluginInfo.incrementObjectsProcessedWithSuccess();
-
+ outcomeText = "";
} catch (AuthorizationDeniedException e) {
state = PluginState.FAILURE;
outcomeText = "Can't retrieve AIP " + aipId + " for lifting transitive hold " + holdId + ".";
@@ -271,6 +275,8 @@ private void processTransitiveAIP(ModelService model, IndexService index, Job ca
LOGGER.error("Error creating event: {}", e.getMessage(), e);
}
}
+
+ return results.getTotalCount();
}
@Override
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/v2/ConfigurableIngestPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/v2/ConfigurableIngestPlugin.java
index db71d4cd93..572bd44193 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/v2/ConfigurableIngestPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/ingest/v2/ConfigurableIngestPlugin.java
@@ -43,7 +43,7 @@ public class ConfigurableIngestPlugin extends DefaultIngestPlugin {
static {
// 2) virus check
- steps.add(new IngestStep(AntivirusPlugin.class.getName(), RodaConstants.PLUGIN_PARAMS_DO_VIRUS_CHECK, true, false,
+ steps.add(new IngestStep(AntivirusPlugin.class.getName(), RodaConstants.PLUGIN_PARAMS_DO_VIRUS_CHECK, true, true,
true, true));
// 3) descriptive metadata validation
steps.add(new IngestStep(DescriptiveMetadataValidationPlugin.class.getName(),
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/preservation/AIPCorruptionRiskAssessmentPlugin.java b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/preservation/AIPCorruptionRiskAssessmentPlugin.java
index fe5d24d96e..d57c5c37f2 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/preservation/AIPCorruptionRiskAssessmentPlugin.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/plugins/base/preservation/AIPCorruptionRiskAssessmentPlugin.java
@@ -243,6 +243,14 @@ private void processAIP(IndexService index, ModelService model, Report report, J
aipFailed = true;
createIncidence(model, index, aip.getId(), pm.getRepresentationId(), pm.getFileDirectoryPath(),
pm.getFileId(), risks.get(0));
+ } catch (RequestNotValidException e) {
+ LOGGER.error("Error retrieving file {} of representation {}", pm.getFileId(),
+ pm.getRepresentationId(), e);
+ ValidationIssue issue = new ValidationIssue(
+ "File " + pm.getFileId() + " of representation " + pm.getRepresentationId() + " of AIP "
+ + pm.getAipId() + " could not be retrieved but the PREMIS file exists: " + e.getMessage());
+ validationReport.addIssue(issue);
+ aipFailed = true;
}
}
}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/repository/job/JobRepository.java b/roda-core/roda-core/src/main/java/org/roda/core/repository/job/JobRepository.java
new file mode 100644
index 0000000000..669a510758
--- /dev/null
+++ b/roda-core/roda-core/src/main/java/org/roda/core/repository/job/JobRepository.java
@@ -0,0 +1,23 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE file at the root of the source
+ * tree and available online at
+ *
+ * https://github.com/keeps/roda
+ */
+package org.roda.core.repository.job;
+
+import org.roda.core.data.v2.jobs.Job;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * JPA Repository for Job entities. Used to store running jobs in the database
+ * before they are flushed to file storage upon completion.
+ *
+ * @author RODA Development Team
+ */
+@Repository
+public interface JobRepository extends JpaRepository {
+ // Standard JPA methods are inherited from JpaRepository
+}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/repository/job/ReportRepository.java b/roda-core/roda-core/src/main/java/org/roda/core/repository/job/ReportRepository.java
new file mode 100644
index 0000000000..f30077b53f
--- /dev/null
+++ b/roda-core/roda-core/src/main/java/org/roda/core/repository/job/ReportRepository.java
@@ -0,0 +1,43 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE file at the root of the source
+ * tree and available online at
+ *
+ * https://github.com/keeps/roda
+ */
+package org.roda.core.repository.job;
+
+import java.util.List;
+
+import org.roda.core.data.v2.jobs.Report;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * JPA Repository for Report entities. Used to store job reports in the database
+ * while the associated job is running, before they are flushed to file storage.
+ *
+ * @author RODA Development Team
+ */
+@Repository
+public interface ReportRepository extends JpaRepository {
+
+ /**
+ * Find all reports associated with a given job ID.
+ *
+ * @param jobId
+ * the job ID to search for
+ * @return list of reports for the specified job
+ */
+ List findByJobId(String jobId);
+
+ /**
+ * Delete all reports associated with a given job ID.
+ *
+ * @param jobId
+ * the job ID whose reports should be deleted
+ */
+ @Transactional
+ void deleteByJobId(String jobId);
+}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/DefaultTransactionalStorageService.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/DefaultTransactionalStorageService.java
index 3b4653cd0f..a389b27bc8 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/DefaultTransactionalStorageService.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/DefaultTransactionalStorageService.java
@@ -28,6 +28,7 @@
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.LiteRODAObject;
import org.roda.core.data.v2.ip.StoragePath;
import org.roda.core.entity.transaction.OperationState;
import org.roda.core.entity.transaction.OperationType;
@@ -48,11 +49,10 @@
public class DefaultTransactionalStorageService implements TransactionalStorageService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTransactionalStorageService.class);
-
+ private final TransactionLogService transactionLogService;
private StorageService stagingStorageService;
private StorageService mainStorageService;
private TransactionLog transaction;
- private final TransactionLogService transactionLogService;
private boolean isInitialized = false;
public DefaultTransactionalStorageService(StorageService mainStorageService, StorageService stagingStorageService,
@@ -288,7 +288,18 @@ public Binary createBinary(StoragePath storagePath, ContentPayload payload, bool
throws GenericException, AlreadyExistsException, RequestNotValidException, AuthorizationDeniedException,
NotFoundException {
TransactionalStoragePathOperationLog operationLog;
- // if storage path is agent we need to register a create or update operation
+
+ try {
+ TransactionalStoragePathOperationLog anyDeletedStoragePathOperation = transactionLogService.getAnyDeletedStoragePathOperation(transaction.getId(),
+ storagePath.toString());
+ if (anyDeletedStoragePathOperation == null && mainStorageService.exists(storagePath)) {
+ throw new AlreadyExistsException("Binary already exists: " + storagePath);
+ }
+ } catch (RODATransactionException e) {
+ throw new GenericException("[transactionId:" + transaction.getId()
+ + "] Failed to create binary for storage path: " + storagePath, e);
+ }
+
if (storagePath.getDirectoryPath() != null && !storagePath.getDirectoryPath().isEmpty()
&& storagePath.getDirectoryPath().getFirst().equals(RodaConstants.STORAGE_DIRECTORY_AGENTS)) {
operationLog = registerOperation(storagePath, OperationType.CREATE_OR_UPDATE);
@@ -439,6 +450,31 @@ public void copy(StorageService fromService, StoragePath fromStoragePath, Path t
}
+ @Override
+ public void importObject(StorageService fromService, LiteRODAObject object, StoragePath toStoragePath,
+ boolean replaceExisting) throws AlreadyExistsException, GenericException, AuthorizationDeniedException,
+ NotFoundException, RequestNotValidException {
+ List operationLogs;
+ if (replaceExisting) {
+ operationLogs = registerOperationForCopy(fromService, toStoragePath, toStoragePath,
+ OperationType.CREATE_OR_UPDATE);
+ } else {
+ operationLogs = registerOperationForCopy(fromService, toStoragePath, toStoragePath, OperationType.CREATE);
+ }
+ try {
+ stagingStorageService.importObject(fromService, object, toStoragePath, replaceExisting);
+ for (TransactionalStoragePathOperationLog operationLog : operationLogs) {
+ updateOperationState(operationLog, OperationState.SUCCESS);
+ }
+ } catch (AlreadyExistsException | NotFoundException | RequestNotValidException | GenericException
+ | AuthorizationDeniedException e) {
+ for (TransactionalStoragePathOperationLog operationLog : operationLogs) {
+ updateOperationState(operationLog, OperationState.FAILURE);
+ }
+ throw e;
+ }
+ }
+
@Override
public void move(StorageService fromService, StoragePath fromStoragePath, StoragePath toStoragePath)
throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException,
@@ -763,7 +799,8 @@ private void handleCreateUpdateOperation(StoragePath storagePath, String version
if (Container.class.isAssignableFrom(rootEntity)) {
mainStorageService.createContainer(storagePath);
} else if (Directory.class.isAssignableFrom(rootEntity)) {
- mainStorageService.createDirectory(storagePath);
+ if (!mainStorageService.exists(storagePath))
+ mainStorageService.createDirectory(storagePath);
} else {
StorageServiceUtils.syncBetweenStorageServices(stagingStorageService, storagePath, mainStorageService,
storagePath, getEntity(storagePath));
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/InputStreamContentPayload.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/InputStreamContentPayload.java
index 6661aca7fc..c85f7b18a1 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/InputStreamContentPayload.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/InputStreamContentPayload.java
@@ -15,7 +15,6 @@
import java.nio.file.StandardCopyOption;
import org.roda.core.common.ProvidesInputStream;
-import org.roda.core.storage.fs.FSUtils;
public class InputStreamContentPayload implements ContentPayload {
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/RangeConsumesOutputStream.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/RangeConsumesOutputStream.java
index 622281fe0a..83e313483c 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/RangeConsumesOutputStream.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/RangeConsumesOutputStream.java
@@ -7,88 +7,80 @@
*/
package org.roda.core.storage;
-import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.Date;
-import org.apache.commons.io.IOUtils;
import org.roda.core.data.v2.ConsumesSkipableOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class RangeConsumesOutputStream implements ConsumesSkipableOutputStream {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RangeConsumesOutputStream.class);
+
private static final String DEFAULT_MIME_TYPE = "application/octet-stream";
- private final Path directAccessPath;
+ private final SeekableContentPayload payload;
+ private final String filename;
private final String mediaType;
+ private final Date lastModified;
+ private final long size;
- public RangeConsumesOutputStream(Path directAccessPath, String mediaType) {
- this.directAccessPath = directAccessPath;
+ public RangeConsumesOutputStream(SeekableContentPayload payload, String filename, Date lastModified, long size,
+ String mediaType) {
+ this.payload = payload;
+ this.filename = filename;
+ this.lastModified = lastModified;
+ this.size = size;
this.mediaType = mediaType;
}
- public RangeConsumesOutputStream(Path directAccessPath) {
- this(directAccessPath, DEFAULT_MIME_TYPE);
+ public RangeConsumesOutputStream(SeekableContentPayload payload, Binary binary) {
+ this(payload, binary, DEFAULT_MIME_TYPE);
+ }
+
+ public RangeConsumesOutputStream(SeekableContentPayload payload, Binary binary, String mediaType) {
+ this.payload = payload;
+ this.filename = binary.getStoragePath().getName();
+ this.lastModified = new Date(); // TODO missing information about binary last modified date
+ this.size = binary.getSizeInBytes();
+ this.mediaType = mediaType;
}
@Override
public void consumeOutputStream(OutputStream out) throws IOException {
- try (InputStream in = Files.newInputStream(directAccessPath)) {
- IOUtils.copyLarge(in, out);
- }
+ payload.writeTo(out, 0, getSize());
}
@Override
public void consumeOutputStream(OutputStream out, int from, int len) throws IOException {
- try (InputStream in = Files.newInputStream(directAccessPath)) {
- IOUtils.copyLarge(in, out, from, len);
- }
+ payload.writeTo(out, from, len);
}
@Override
public void consumeOutputStream(OutputStream out, long from, long end) {
try {
- File file = directAccessPath.toFile();
- byte[] buffer = new byte[1024];
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
- long pos = from;
- randomAccessFile.seek(pos);
- while (pos < end) {
- randomAccessFile.read(buffer);
- out.write(buffer);
- pos += buffer.length;
- }
- out.flush();
- }
+ payload.writeTo(out, from, end - from + 1);
} catch (IOException e) {
- // ignore
+ // This error occurs when web browser cancels stream
+ // Which can normally happen in HTTP streaming
+ LOGGER.trace("Error writing to output stream", e);
}
}
@Override
public Date getLastModified() {
- try {
- return new Date(Files.getLastModifiedTime(directAccessPath).toMillis());
- } catch (IOException e) {
- return null;
- }
+ return lastModified;
}
@Override
public long getSize() {
- try {
- return Files.size(directAccessPath);
- } catch (IOException e) {
- return -1;
- }
+ return size;
}
@Override
public String getFileName() {
- return directAccessPath.getFileName().toString();
+ return filename;
}
@Override
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/SeekableContentPayload.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/SeekableContentPayload.java
new file mode 100644
index 0000000000..37bfc41623
--- /dev/null
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/SeekableContentPayload.java
@@ -0,0 +1,14 @@
+package org.roda.core.storage;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface SeekableContentPayload extends ContentPayload {
+ /**
+ * Writes a specific range of the content to the output stream.
+ * * @param out The output stream to write to
+ * @param offset The start byte position (inclusive)
+ * @param length The number of bytes to write
+ */
+ void writeTo(OutputStream out, long offset, long length) throws IOException;
+}
\ No newline at end of file
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageService.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageService.java
index a8877004af..d5a0ddec1d 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageService.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageService.java
@@ -18,7 +18,9 @@
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.LiteRODAObject;
import org.roda.core.data.v2.ip.StoragePath;
+import org.roda.core.transaction.RODATransactionException;
/**
*
@@ -355,6 +357,12 @@ void copy(StorageService fromService, StoragePath fromStoragePath, StoragePath t
void copy(StorageService fromService, StoragePath fromStoragePath, Path toPath, String resource,
boolean replaceExisting) throws AlreadyExistsException, GenericException, AuthorizationDeniedException;
+ default void importObject(StorageService fromService, LiteRODAObject object, StoragePath toStoragePath,
+ boolean replaceExisting) throws AlreadyExistsException, GenericException, AuthorizationDeniedException,
+ NotFoundException, RequestNotValidException {
+ throw new UnsupportedOperationException("Not supported yet.");
+ };
+
/**
* Move resources from another (or the same) storage service.
*
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceUtils.java
index dcb6ae154c..f7c9bac046 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceUtils.java
@@ -24,6 +24,7 @@
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.v2.ip.StoragePath;
+import org.roda.core.transaction.RODATransactionException;
/**
* Storage Service related and independent utility class
@@ -113,14 +114,14 @@ public static void syncBetweenStorageServices(StorageService fromService, Storag
*/
public static void copyBetweenStorageServices(StorageService fromService, StoragePath fromStoragePath,
StorageService toService, StoragePath toStoragePath, Class extends Entity> rootEntity) throws GenericException,
- RequestNotValidException, NotFoundException, AlreadyExistsException, AuthorizationDeniedException {
+ RequestNotValidException, NotFoundException, AlreadyExistsException, AuthorizationDeniedException {
copyOrMoveBetweenStorageServices(fromService, fromStoragePath, toService, toStoragePath, rootEntity, true, false);
}
private static void copyOrMoveBetweenStorageServices(StorageService fromService, StoragePath fromStoragePath,
StorageService toService, StoragePath toStoragePath, Class extends Entity> rootEntity, boolean copy, boolean sync)
- throws GenericException, RequestNotValidException, NotFoundException, AlreadyExistsException,
- AuthorizationDeniedException {
+ throws GenericException, RequestNotValidException, NotFoundException, AlreadyExistsException,
+ AuthorizationDeniedException {
if (Container.class.isAssignableFrom(rootEntity)) {
toService.createContainer(toStoragePath);
boolean recursive = false;
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceWrapper.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceWrapper.java
index 5b329f847e..7094965c04 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceWrapper.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/StorageServiceWrapper.java
@@ -20,6 +20,7 @@
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.LiteRODAObject;
import org.roda.core.data.v2.ip.StoragePath;
public class StorageServiceWrapper implements StorageService {
@@ -185,6 +186,14 @@ public void copy(StorageService fromService, StoragePath fromStoragePath, Path t
storageService.copy(fromService, fromStoragePath, toPath, resource, replaceExisting);
}
+ @Override
+ public void importObject(StorageService fromService, LiteRODAObject object, StoragePath toStoragePath,
+ boolean replaceExisting) throws AlreadyExistsException, GenericException, AuthorizationDeniedException,
+ NotFoundException, RequestNotValidException {
+ RodaCoreFactory.checkIfWriteIsAllowedAndIfFalseThrowException(nodeType);
+ storageService.importObject(fromService, object, toStoragePath, replaceExisting);
+ }
+
@Override
public void move(StorageService fromService, StoragePath fromStoragePath, StoragePath toStoragePath)
throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException,
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FSPathContentPayload.java b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FSPathContentPayload.java
index b0da1590e7..fe4febdaf6 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FSPathContentPayload.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/storage/fs/FSPathContentPayload.java
@@ -9,19 +9,22 @@
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import org.roda.core.storage.ContentPayload;
+import org.apache.commons.io.IOUtils;
+import org.roda.core.storage.SeekableContentPayload;
/**
* Class that implements {@code ContentPayload} for File System
*
* @author Luis Faria
*/
-public class FSPathContentPayload implements ContentPayload {
+public class FSPathContentPayload implements SeekableContentPayload {
private final Path path;
@@ -44,4 +47,41 @@ public URI getURI() throws IOException, UnsupportedOperationException {
return path.toUri();
}
+ @Override
+ public void writeTo(OutputStream out, long offset, long length) throws IOException {
+ // 1. Use NIO InputStream (Efficient: uses native pread/lseek)
+ try (InputStream is = Files.newInputStream(path)) {
+
+ // 2. Seek to the start position (Instant operation on files)
+ long skipped = is.skip(offset);
+ if (skipped < offset) {
+ // File is smaller than the offset requested
+ return;
+ }
+
+ // 3. Transfer only the requested amount
+ byte[] buffer = new byte[8192]; // Standard 8KB buffer
+ long remaining = length;
+ int bytesRead;
+
+ // Loop while we still need data AND we haven't hit EOF
+ while (remaining > 0) {
+ // Determine how much to read: either the full buffer or the remaining bytes
+ int bytesToRead = (int) Math.min(buffer.length, remaining);
+
+ bytesRead = is.read(buffer, 0, bytesToRead);
+
+ if (bytesRead == -1) {
+ break; // End of file reached prematurely
+ }
+
+ // Critical: Only write the bytes we actually read
+ out.write(buffer, 0, bytesRead);
+
+ remaining -= bytesRead;
+ }
+
+ }
+ }
+
}
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 cb85d8e6c9..47a626c2d4 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
@@ -34,6 +34,7 @@
import org.roda.core.data.exceptions.NotFoundException;
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.utils.JsonUtils;
+import org.roda.core.data.v2.LiteRODAObject;
import org.roda.core.data.v2.ip.ShallowFile;
import org.roda.core.data.v2.ip.ShallowFiles;
import org.roda.core.data.v2.ip.StoragePath;
@@ -72,9 +73,8 @@
*/
public class FileStorageService implements StorageService {
- private static final Logger LOGGER = LoggerFactory.getLogger(FileStorageService.class);
-
public static final String HISTORY_SUFFIX = "-history";
+ private static final Logger LOGGER = LoggerFactory.getLogger(FileStorageService.class);
private static final String HISTORY_DATA_FOLDER = "data";
private static final String HISTORY_METADATA_FOLDER = "metadata";
@@ -579,6 +579,26 @@ public void copy(StorageService fromService, StoragePath fromStoragePath, Path t
}
}
+ @Override
+ public void importObject(StorageService fromService, LiteRODAObject object, StoragePath toStoragePath,
+ boolean replaceExisting) throws AlreadyExistsException, GenericException, NotFoundException,
+ AuthorizationDeniedException, RequestNotValidException {
+ StoragePath fromPath = ModelUtils.getStoragePath(object);
+ if (!fromService.exists(fromPath)) {
+ throw new NotFoundException("Source Path does not exist: " + fromPath);
+ }
+
+ if (exists(toStoragePath)) {
+ if (replaceExisting) {
+ // workaround
+ deleteResource(toStoragePath);
+ } else {
+ throw new AlreadyExistsException("Destination already exists: " + toStoragePath);
+ }
+ }
+ copy(fromService, fromPath, toStoragePath);
+ }
+
@Override
public void move(StorageService fromService, StoragePath fromStoragePath, StoragePath toStoragePath)
throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException,
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManager.java b/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManager.java
index a54235a049..17c7fe77f2 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManager.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManager.java
@@ -8,15 +8,11 @@
package org.roda.core.transaction;
import java.nio.file.Path;
-import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.roda.core.config.ConfigurationManager;
import org.roda.core.data.common.RodaConstants;
@@ -26,8 +22,6 @@
import org.roda.core.data.exceptions.RequestNotValidException;
import org.roda.core.data.v2.IsRODAObject;
import org.roda.core.data.v2.LiteOptionalWithCause;
-import org.roda.core.data.v2.jobs.Job;
-import org.roda.core.data.v2.jobs.PluginState;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.entity.transaction.TransactionLog;
import org.roda.core.model.ModelService;
@@ -94,65 +88,69 @@ public TransactionalContext beginTransaction(TransactionLog.TransactionRequestTy
}
public void runPluginInTransaction(Plugin plugin, List objectsToBeProcessed)
- throws RODATransactionException, PluginException {
+ throws PluginException {
String requestUUID = plugin.getParameterValues().getOrDefault(RodaConstants.PLUGIN_PARAMS_LOCK_REQUEST_UUID,
IdUtils.createUUID());
plugin.getParameterValues().put(RodaConstants.PLUGIN_PARAMS_LOCK_REQUEST_UUID, requestUUID);
- TransactionalContext context = beginTransaction(TransactionLog.TransactionRequestType.JOB,
- UUID.fromString(requestUUID));
+ TransactionalContext context;
+ try {
+ context = beginTransaction(TransactionLog.TransactionRequestType.JOB, UUID.fromString(requestUUID));
+ } catch (RODATransactionException e) {
+ throw new PluginException("Failed to begin transaction for plugin execution", e);
+ }
+
UUID transactionId = context.transactionLog().getId();
LOGGER.debug("[transactionId:{}] Running the plugin {} in a transaction", transactionId, plugin.getName());
Date initDate = new Date();
+ List reports;
try {
plugin.execute(context.indexService(), context.transactionalModelService(), objectsToBeProcessed);
- } catch (PluginException e) {
- LOGGER.error("[transactionId:{}] Plugin execution failed: {}", transactionId, e.getMessage(), e);
- throw e;
+ reports = RODATransactionManagerUtils.getReportsForTransaction(plugin, transactionId, mainModelService);
+ } catch (Exception e) {
+ LOGGER.error("[transactionId:{}] Error during plugin execution, rolling back transaction", transactionId, e);
+ rollbackTransaction(transactionId);
+ throw new PluginException("Error during plugin execution, transaction was rolled back", e);
} finally {
- processPluginExecutionResult(plugin, transactionId, initDate);
// remove locks if any
PluginHelper.releaseObjectLock(plugin);
}
+
+ // Check if any of the reports indicate that the transaction should be rolled
+ // back
+ if (RODATransactionManagerUtils.shouldRollback(plugin, RODATransactionManagerUtils.getFailedReports(reports))) {
+ rollbackTransaction(transactionId);
+ processPluginExecutionResult(transactionId, initDate, reports, false);
+ } else {
+ // If everything is fine, commit the transaction
+ try {
+ endTransaction(transactionId);
+ processPluginExecutionResult(transactionId, initDate, reports, true);
+ } catch (RODATransactionException e) {
+ // If commit fails, we should attempt to rollback and log the error
+ rollbackTransaction(transactionId);
+ processPluginExecutionResult(transactionId, initDate, reports, false);
+ throw new PluginException("Failed to commit transaction for plugin execution, transaction was rolled back", e);
+ }
+ }
}
- private void processPluginExecutionResult(Plugin plugin, UUID transactionId, Date initDate)
- throws RODATransactionException {
+ private void processPluginExecutionResult(UUID transactionId, Date initDate, List relatedReports,
+ boolean success) {
try {
- Job job = mainModelService.retrieveJob(PluginHelper.getJobId(plugin));
- List relatedReports = RODATransactionManagerUtils.getReportsForTransaction(job.getId(), transactionId,
- mainModelService);
-
- List failedReports = relatedReports.stream()
- .filter(report -> PluginState.FAILURE.equals(report.getPluginState())).toList();
-
- List nonFailedReports = relatedReports.stream()
- .filter(report -> !PluginState.FAILURE.equals(report.getPluginState())).toList();
-
- String noRollback = plugin.getParameterValues()
- .getOrDefault(RodaConstants.PLUGIN_PARAM_SKIP_ROLLBACK_ON_VALIDATION_FAILURE, "");
- Set noRollbackPlugins = Arrays.stream(noRollback.split(",")).map(String::trim).filter(s -> !s.isEmpty())
- .collect(Collectors.toSet());
-
- boolean shouldRollback = failedReports.stream().flatMap(fr -> {
- List nested = fr.getReports();
- return nested == null ? Stream.empty() : nested.stream();
- }).filter(nr -> PluginState.FAILURE.equals(nr.getPluginState())).map(Report::getPlugin)
- .filter(java.util.Objects::nonNull).anyMatch(pluginName -> !noRollbackPlugins.contains(pluginName));
+ if (success) {
+ RODATransactionManagerUtils.createTransactionSuccessReports(relatedReports, transactionId, initDate,
+ mainModelService);
+ } else {
+ List failedReports = RODATransactionManagerUtils.getFailedReports(relatedReports);
+ List nonFailedReports = RODATransactionManagerUtils.getNonFailedReports(relatedReports);
- if (shouldRollback) {
- rollbackTransaction(transactionId);
RODATransactionManagerUtils.createTransactionFailureReports(failedReports, nonFailedReports, transactionId,
initDate, mainModelService);
- } else {
- endTransaction(transactionId);
- RODATransactionManagerUtils.createTransactionSuccessReports(relatedReports, transactionId, initDate,
- mainModelService);
}
- } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException e) {
- throw new RODATransactionException(
- "Error handling plugin result for plugin: " + plugin.getName() + " with transaction ID: " + transactionId, e);
+ } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) {
+ LOGGER.error("Critical: Failed to generate reports for transaction {}", transactionId, e);
}
}
@@ -176,29 +174,32 @@ public void endTransaction(UUID transactionID) throws RODATransactionException {
transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.COMMITTED);
}
- public void rollbackTransaction(UUID transactionID) throws RODATransactionException {
+ public void rollbackTransaction(UUID transactionID) {
TransactionalContext context = transactionsContext.get(transactionID);
if (context == null) {
- throw new RODATransactionException("No transaction context found for ID: " + transactionID);
- }
-
- transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLLING_BACK);
-
- try {
- if (context.transactionalStorageService() != null) {
- context.transactionalStorageService().rollback();
- }
-
- if (context.transactionalModelService() != null) {
- context.transactionalModelService().rollback();
+ LOGGER.error("No transaction context found for ID: {}", transactionID);
+ } else {
+ try {
+ transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLLING_BACK);
+ if (context.transactionalStorageService() != null) {
+ context.transactionalStorageService().rollback();
+ }
+
+ if (context.transactionalModelService() != null) {
+ context.transactionalModelService().rollback();
+ }
+
+ transactionsContext.remove(transactionID);
+ transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLLED_BACK);
+ } catch (Exception e) {
+ LOGGER.error("Error during rollback of transaction: {}", transactionID, e);
+ try {
+ transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLL_BACK_FAILED);
+ } catch (RODATransactionException ex) {
+ LOGGER.error("Error updating transaction log status to ROLL_BACK_FAILED for transaction: {}", transactionID,
+ ex);
+ }
}
-
- transactionsContext.remove(transactionID);
- transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLLED_BACK);
-
- } catch (Exception e) {
- transactionLogService.changeStatus(transactionID, TransactionLog.TransactionStatus.ROLL_BACK_FAILED);
- throw new RODATransactionException("Error during rollback of transaction: " + transactionID, e);
}
}
diff --git a/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManagerUtils.java b/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManagerUtils.java
index 4d37dc45ae..8c5fa73015 100644
--- a/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManagerUtils.java
+++ b/roda-core/roda-core/src/main/java/org/roda/core/transaction/RODATransactionManagerUtils.java
@@ -9,20 +9,28 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.roda.core.common.iterables.CloseableIterable;
+import org.roda.core.data.common.RodaConstants;
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.IsRODAObject;
import org.roda.core.data.v2.common.OptionalWithCause;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.jobs.PluginState;
import org.roda.core.data.v2.jobs.Report;
import org.roda.core.model.ModelService;
+import org.roda.core.plugins.Plugin;
+import org.roda.core.plugins.PluginHelper;
import org.roda.core.util.IdUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,27 +41,31 @@
public class RODATransactionManagerUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(RODATransactionManagerUtils.class);
- public static List getReportsForTransaction(String jobId, UUID transactionId, ModelService model)
- throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException,
- RODATransactionException {
- List reports = new ArrayList<>();
- try (CloseableIterable> reportList = model.listJobReports(jobId)) {
- for (OptionalWithCause optionalReport : reportList) {
- if (optionalReport.isPresent()) {
- Report innerReport = optionalReport.get();
- if (innerReport.getTransactionId().equals(transactionId.toString())) {
- reports.add(innerReport);
+ public static List getReportsForTransaction(Plugin plugin, UUID transactionId,
+ ModelService model) throws RODATransactionException {
+ try {
+ Job job = model.retrieveJob(PluginHelper.getJobId(plugin));
+ List reports = new ArrayList<>();
+ try (CloseableIterable> reportList = model.listJobReports(job.getId())) {
+ for (OptionalWithCause optionalReport : reportList) {
+ if (optionalReport.isPresent()) {
+ Report innerReport = optionalReport.get();
+ if (innerReport.getTransactionId().equals(transactionId.toString())) {
+ reports.add(innerReport);
+ }
}
}
}
- } catch (NotFoundException | IOException e) {
+ return reports;
+ } catch (NotFoundException | IOException | RequestNotValidException | GenericException
+ | AuthorizationDeniedException e) {
throw new RODATransactionException("Error retrieving reports for transaction ID: " + transactionId, e);
}
- return reports;
}
public static void createTransactionFailureReports(List failedReports, List nonFailedReports,
- UUID transactionId, Date initDate, ModelService model) throws RODATransactionException {
+ UUID transactionId, Date initDate, ModelService model)
+ throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException {
for (Report report : nonFailedReports) {
String details = "This transaction failed because a related transaction also failed";
@@ -67,7 +79,8 @@ public static void createTransactionFailureReports(List failedReports, L
}
public static void createTransactionSuccessReports(List relatedReports, UUID transactionId, Date initDate,
- ModelService model) throws RODATransactionException {
+ ModelService model)
+ throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException {
String details = "Transaction was committed successfully.";
for (Report report : relatedReports) {
@@ -76,31 +89,49 @@ public static void createTransactionSuccessReports(List relatedReports,
}
public static void createTransactionReportItem(Report innerReport, UUID transactionId, PluginState state,
- Date initDate, String details, ModelService model) throws RODATransactionException {
- try {
- Job job = model.retrieveJob(innerReport.getJobId());
- innerReport.setTotalSteps(innerReport.getTotalSteps() + 1);
-
- Report reportItem = new Report();
- reportItem.injectLineSeparator(System.lineSeparator());
- reportItem.setId(IdUtils.getJobReportId(innerReport.getJobId(), innerReport.getSourceObjectId(),
- innerReport.getOutcomeObjectId()));
- reportItem.setJobId(innerReport.getJobId());
- reportItem.setSourceAndOutcomeObjectId(innerReport.getSourceObjectId(), innerReport.getOutcomeObjectId());
- reportItem.setTitle("RODA Transaction Manager");
- reportItem.setPlugin(RODATransactionManager.class.getName());
- reportItem.setPluginName("RODA Transaction Manager");
- reportItem.setPluginDetails(String.format("[Transaction ID: %s] %s", transactionId, details));
- reportItem.setPluginState(state);
- reportItem.setOutcomeObjectState(innerReport.getOutcomeObjectState());
- reportItem.setDateCreated(initDate);
- reportItem.setDateUpdated(new Date());
- reportItem.setHtmlPluginDetails(innerReport.isHtmlPluginDetails());
- innerReport.addReport(reportItem);
-
- model.createOrUpdateJobReport(innerReport, job);
- } catch (RequestNotValidException | GenericException | NotFoundException | AuthorizationDeniedException e) {
- throw new RODATransactionException("Error adding report item for transaction ID: " + transactionId, e);
- }
+ Date initDate, String details, ModelService model)
+ throws AuthorizationDeniedException, RequestNotValidException, NotFoundException, GenericException {
+
+ Job job = model.retrieveJob(innerReport.getJobId());
+ innerReport.setTotalSteps(innerReport.getTotalSteps() + 1);
+
+ Report reportItem = new Report();
+ reportItem.injectLineSeparator(System.lineSeparator());
+ reportItem.setId(IdUtils.getJobReportId(innerReport.getJobId(), innerReport.getSourceObjectId(),
+ innerReport.getOutcomeObjectId()));
+ reportItem.setJobId(innerReport.getJobId());
+ reportItem.setSourceAndOutcomeObjectId(innerReport.getSourceObjectId(), innerReport.getOutcomeObjectId());
+ reportItem.setTitle("RODA Transaction Manager");
+ reportItem.setPlugin(RODATransactionManager.class.getName());
+ reportItem.setPluginName("RODA Transaction Manager");
+ reportItem.setPluginDetails(String.format("[Transaction ID: %s] %s", transactionId, details));
+ reportItem.setPluginState(state);
+ reportItem.setOutcomeObjectState(innerReport.getOutcomeObjectState());
+ reportItem.setDateCreated(initDate);
+ reportItem.setDateUpdated(new Date());
+ reportItem.setHtmlPluginDetails(innerReport.isHtmlPluginDetails());
+ innerReport.addReport(reportItem);
+
+ model.createOrUpdateJobReport(innerReport, job);
+ }
+
+ public static boolean shouldRollback(Plugin plugin, List failedReports) {
+ String noRollback = plugin.getParameterValues()
+ .getOrDefault(RodaConstants.PLUGIN_PARAM_SKIP_ROLLBACK_ON_VALIDATION_FAILURE, "");
+
+ Set noRollbackPlugins = Arrays.stream(noRollback.split(",")).map(String::trim).filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+
+ return failedReports.stream().flatMap(fr -> fr.getReports() == null ? Stream.empty() : fr.getReports().stream())
+ .filter(nr -> PluginState.FAILURE.equals(nr.getPluginState())).map(Report::getPlugin)
+ .filter(java.util.Objects::nonNull).anyMatch(pluginName -> !noRollbackPlugins.contains(pluginName));
+ }
+
+ public static List getFailedReports(List reports) {
+ return reports.stream().filter(report -> PluginState.FAILURE.equals(report.getPluginState())).toList();
+ }
+
+ public static List getNonFailedReports(List reports) {
+ return reports.stream().filter(report -> !PluginState.FAILURE.equals(report.getPluginState())).toList();
}
}
diff --git a/roda-core/roda-core/src/main/resources/config/roda-core.properties b/roda-core/roda-core/src/main/resources/config/roda-core.properties
index e92b932660..753e0b7584 100644
--- a/roda-core/roda-core/src/main/resources/config/roda-core.properties
+++ b/roda-core/roda-core/src/main/resources/config/roda-core.properties
@@ -81,9 +81,10 @@ core.storage.type=FILESYSTEM
##########################################################################
core.solr.type=CLOUD
core.solr.cloud.urls=localhost:2181
-core.solr.cloud.connect.timeout_ms=60000
+core.solr.cloud.connect.timeout_ms=300000
core.solr.cloud.healthcheck.retries=100
core.solr.cloud.healthcheck.timeout_ms=10000
+core.solr.cloud.zk.client.timeout_ms=600000
# Stemming and stopwords configuration for "*_txt" fields
# When missing or blank Solr uses the "text_general" type for "*_txt"
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 278efa89f8..40ee50fc25 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
@@ -76,7 +76,7 @@ core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.download
core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.getSelectedTransferredResources = transfer.read
core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.createTransferredResource = transfer.create
core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.reindexResources = transfer.create
-core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.renameTransferredResource = transfer.create
+core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.renameTransferredResource = transfer.update
core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.refreshTransferResource = transfer.read
core.roles.org.roda.wui.api.v2.controller.TransferredResourceController.moveTransferredResources = transfer.create
diff --git a/roda-ui/roda-wui/pom.xml b/roda-ui/roda-wui/pom.xml
index 6d2bb0b257..344f96445c 100644
--- a/roda-ui/roda-wui/pom.xml
+++ b/roda-ui/roda-wui/pom.xml
@@ -77,7 +77,7 @@
true
- org.project
+ org.gwtproject
gwt-dev
@@ -124,8 +124,6 @@
-Droda.home=${env.HOME}/.roda_local
-Droda.environment.collect.version=false
-Dgwt.codeServerPort=9876 -Xdebug
-
- -Dorg.springframework.boot.logging.LoggingSystem=none
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5007
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
@@ -182,8 +180,6 @@
-Droda.home=${env.HOME}/.roda_central
-Droda.environment.collect.version=false
-Dgwt.codeServerPort=9876 -Xdebug
-
- -Dorg.springframework.boot.logging.LoggingSystem=none
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5006
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
@@ -238,9 +234,6 @@
-Dgwt.codeServerPort=9876 -Xdebug
-
- -Dorg.springframework.boot.logging.LoggingSystem=none
-
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005
-Droda.environment.collect.version=false
--add-opens java.base/java.util=ALL-UNNAMED
@@ -351,7 +344,7 @@
gwt-servlet-jakarta
- com.ekotrope
+ org.roda-community
gwt-completablefuture
1.0.1
@@ -470,7 +463,7 @@
jakarta.ws.rs-api
- org.fusesource.restygwt
+ org.roda-community
restygwt
@@ -486,7 +479,7 @@
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.8.14
+ 2.8.16
org.webjars
diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/RODA.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/RODA.java
index 43f2d6f28b..055ddbdfe8 100644
--- a/roda-ui/roda-wui/src/main/java/org/roda/wui/RODA.java
+++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/RODA.java
@@ -23,7 +23,7 @@
UserDetailsServiceAutoConfiguration.class})
@ComponentScan(basePackages = {"org.roda.*"})
@EnableJpaRepositories(basePackages = "org.roda.core.repository")
-@EntityScan(basePackages = "org.roda.core.entity")
+@EntityScan(basePackages = {"org.roda.core.entity", "org.roda.core.data.v2.jobs"})
@ServletComponentScan
@EnableScheduling
public class RODA {
diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DisposalConfirmationController.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DisposalConfirmationController.java
index ba45f0526c..bc7fb4cdfc 100644
--- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DisposalConfirmationController.java
+++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/DisposalConfirmationController.java
@@ -115,7 +115,7 @@ public ResponseEntity process(RequestContext requestConte
// delegate and return
return ApiUtils.okResponse(
- disposalConfirmationService.createDisposalConfirmationReport(disposalConfirmationId, toPrint), null);
+ disposalConfirmationService.createDisposalConfirmationReport(requestContext.getModelService(), disposalConfirmationId, toPrint), null);
}
});
}
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 bc36fc55c5..ba82e427df 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
@@ -102,21 +102,22 @@ public class FilesController implements FileRestService, Exportable {
@ApiResponse(responseCode = "404", description = "Not found", content = @Content(schema = @Schema(implementation = ErrorResponseMessage.class)))})
public ResponseEntity previewBinary(
@Parameter(description = "The UUID of the existing file", required = true) @PathVariable(name = "uuid") String fileUUID,
+ @Parameter(description = "Use to set the content disposition inline") @RequestParam(name = "inline", defaultValue = "true", required = false) boolean inline,
@RequestHeader HttpHeaders headers) {
return requestHandler.processRequest(new RequestHandler.RequestProcessor>() {
@Override
public ResponseEntity process(RequestContext requestContext,
RequestControllerAssistant controllerAssistant) throws RODAException, RESTException {
- controllerAssistant.setRelatedObjectId(fileUUID);
controllerAssistant.setParameters(RodaConstants.CONTROLLER_FILE_UUID_PARAM, fileUUID);
List fileFields = new ArrayList<>(RodaConstants.FILE_FIELDS_TO_RETURN);
fileFields.add(RodaConstants.FILE_ISDIRECTORY);
IndexedFile file = indexService.retrieve(IndexedFile.class, fileUUID, fileFields);
+ controllerAssistant.setRelatedObjectId(file.getAipId());
controllerAssistant.checkObjectPermissions(requestContext.getUser(), file);
RangeConsumesOutputStream stream = filesService.retrieveAIPRepresentationRangeStream(requestContext, file);
- return ApiUtils.rangeResponse(headers, stream);
+ return ApiUtils.rangeResponse(headers, stream, inline);
}
});
}
@@ -136,7 +137,8 @@ public ResponseEntity process(RequestContext requestConte
fileFields.add(RodaConstants.FILE_ISDIRECTORY);
IndexedFile file = indexService.retrieve(IndexedFile.class, fileUUID, fileFields);
controllerAssistant.setRelatedObjectId(file.getAipId());
- controllerAssistant.setParameters(RodaConstants.CONTROLLER_FILE_UUID_PARAM, fileUUID, RodaConstants.CONTROLLER_FILE_ID_PARAM, file.getId());
+ controllerAssistant.setParameters(RodaConstants.CONTROLLER_FILE_UUID_PARAM, fileUUID,
+ RodaConstants.CONTROLLER_FILE_ID_PARAM, file.getId());
controllerAssistant.checkObjectPermissions(requestContext.getUser(), file);
diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/MembersController.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/MembersController.java
index 8a71b7de60..7b20c7f465 100644
--- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/MembersController.java
+++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/MembersController.java
@@ -335,7 +335,7 @@ public Void deleteAccessKey(String accessKeyId) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM);
+ controllerAssistant.registerAction(requestContext, state);
}
}
@@ -360,7 +360,8 @@ public AccessKeys getAccessKeysByUser(String username) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM);
+ controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_USERNAME_PARAM,
+ username);
}
}
@@ -380,7 +381,8 @@ public AccessKey getAccessKey(String accessKeyId) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM);
+ controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_ID_PARAM,
+ accessKeyId);
}
}
@@ -414,7 +416,7 @@ public AccessToken authenticate(@RequestBody String token) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM);
+ controllerAssistant.registerAction(requestContext, state);
}
}
@@ -437,8 +439,8 @@ public AccessKey regenerateAccessKey(String id, @RequestBody CreateAccessKeyRequ
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM,
- regenerateAccessKeyRequest);
+ controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_ID_PARAM, id,
+ RodaConstants.CONTROLLER_ACCESS_KEY_EXP_DATE_PARAM, regenerateAccessKeyRequest.getExpirationDate());
}
}
@@ -465,8 +467,9 @@ public AccessKey createAccessKey(String id, @RequestBody CreateAccessKeyRequest
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM,
- accessKeyRequest);
+ controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_NAME_PARAM,
+ accessKeyRequest.getName(), RodaConstants.CONTROLLER_ACCESS_KEY_EXP_DATE_PARAM,
+ accessKeyRequest.getExpirationDate());
}
}
@@ -488,7 +491,8 @@ public AccessKey revokeAccessKey(String accessKeyId) {
state = LogEntryState.FAILURE;
throw new RESTException(e);
} finally {
- controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_PARAM);
+ controllerAssistant.registerAction(requestContext, state, RodaConstants.CONTROLLER_ACCESS_KEY_ID_PARAM,
+ accessKeyId);
}
}
diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/RequestHandler.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/RequestHandler.java
index df165d79bf..059e4e1081 100644
--- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/RequestHandler.java
+++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/controller/RequestHandler.java
@@ -9,14 +9,12 @@
import java.io.IOException;
-import io.micrometer.core.annotation.Timed;
import org.roda.core.RodaCoreFactory;
import org.roda.core.data.common.RodaConstants;
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.RODAException;
import org.roda.core.data.v2.log.LogEntryState;
import org.roda.core.entity.transaction.TransactionLog;
-import org.roda.core.transaction.RODATransactionException;
import org.roda.core.transaction.RODATransactionManager;
import org.roda.core.transaction.TransactionalContext;
import org.roda.wui.api.v2.exceptions.RESTException;
@@ -28,6 +26,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import io.micrometer.core.annotation.Timed;
import jakarta.servlet.http.HttpServletRequest;
/**
@@ -108,19 +107,15 @@ private T processRequest(RequestProcessor processor, Class> returnClass
controllerAssistant.registerAction(requestContext, controllerAssistant.getRelatedObjectId(), state,
controllerAssistant.getParameters());
}
- try {
- if (isAValidTransactionalContext(isTransactional) && transactionalContext != null
- && state != LogEntryState.SUCCESS) {
- transactionManager.rollbackTransaction(transactionalContext.transactionLog().getId());
- }
- } catch (RODATransactionException ex) {
- LOGGER.error("Error rolling back transaction", ex);
+ if (isAValidTransactionalContext(isTransactional) && transactionalContext != null
+ && state != LogEntryState.SUCCESS) {
+ transactionManager.rollbackTransaction(transactionalContext.transactionLog().getId());
}
}
}
private boolean isAValidTransactionalContext(boolean isTransactional) {
- if(transactionManager != null && transactionManager.isInitialized()) {
+ if (transactionManager != null && transactionManager.isInitialized()) {
// Check if the current node is not a read-only node
boolean writeIsAllowed = RodaCoreFactory.checkIfWriteIsAllowed(RodaCoreFactory.getNodeType());
return writeIsAllowed && isTransactional;
@@ -132,4 +127,4 @@ public interface RequestProcessor {
T process(RequestContext requestContext, RequestControllerAssistant controllerAssistant)
throws RODAException, RESTException, IOException;
}
-}
\ No newline at end of file
+}
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
index 682b102928..2deee1890a 100644
--- 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
@@ -7,6 +7,10 @@
*/
package org.roda.wui.api.v2.services;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
import org.roda.core.data.exceptions.AuthorizationDeniedException;
import org.roda.core.data.exceptions.GenericException;
import org.roda.core.data.exceptions.NotFoundException;
@@ -17,15 +21,14 @@
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.Binary;
+import org.roda.core.storage.ContentPayload;
import org.roda.core.storage.DirectResourceAccess;
import org.roda.core.storage.RangeConsumesOutputStream;
+import org.roda.core.storage.SeekableContentPayload;
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
@@ -33,13 +36,21 @@
@Service
public class DIPFileService {
public RangeConsumesOutputStream retrieveDIPFileRangeStream(RequestContext requestContext, DIPFile dipfile)
- throws RequestNotValidException {
+ throws RequestNotValidException {
ModelService model = requestContext.getModelService();
if (!dipfile.isDirectory()) {
final RangeConsumesOutputStream stream;
try {
- DirectResourceAccess directDIPFileAccess = model.getDirectAccess(dipfile);
- stream = new RangeConsumesOutputStream(directDIPFileAccess.getPath());
+ Binary binary = model.getBinary(dipfile);
+ ContentPayload payload = binary.getContent();
+
+ if (payload instanceof SeekableContentPayload) {
+ SeekableContentPayload seekable = (SeekableContentPayload) payload;
+
+ stream = new RangeConsumesOutputStream(seekable, binary);
+ } else {
+ throw new RequestNotValidException("Range stream for file unsupported");
+ }
return stream;
} catch (RequestNotValidException | GenericException | AuthorizationDeniedException | NotFoundException e) {
throw new RuntimeException(e);
@@ -50,7 +61,7 @@ public RangeConsumesOutputStream retrieveDIPFileRangeStream(RequestContext reque
}
public StreamResponse retrieveDIPFileStreamResponse(RequestContext requestContext, DIPFile dipFile)
- throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
+ throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException {
ModelService model = requestContext.getModelService();
final ConsumesOutputStream stream;
diff --git a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DisposalConfirmationService.java b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DisposalConfirmationService.java
index c0e07b01cd..3a5a3fafc8 100644
--- a/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DisposalConfirmationService.java
+++ b/roda-ui/roda-wui/src/main/java/org/roda/wui/api/v2/services/DisposalConfirmationService.java
@@ -7,6 +7,7 @@
*/
package org.roda.wui.api.v2.services;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
@@ -41,8 +42,10 @@
import org.roda.core.data.v2.disposal.confirmation.DisposalConfirmationForm;
import org.roda.core.data.v2.generics.MetadataValue;
import org.roda.core.data.v2.index.select.SelectedItems;
+import org.roda.core.data.v2.ip.StoragePath;
import org.roda.core.data.v2.jobs.Job;
import org.roda.core.data.v2.user.User;
+import org.roda.core.model.ModelService;
import org.roda.core.model.utils.ModelUtils;
import org.roda.core.plugins.base.disposal.confirmation.CreateDisposalConfirmationPlugin;
import org.roda.core.plugins.base.disposal.confirmation.DeleteDisposalConfirmationPlugin;
@@ -50,6 +53,7 @@
import org.roda.core.plugins.base.disposal.confirmation.PermanentlyDeleteRecordsPlugin;
import org.roda.core.plugins.base.disposal.confirmation.RestoreRecordsPlugin;
import org.roda.core.storage.DefaultStoragePath;
+import org.roda.core.storage.DirectResourceAccess;
import org.roda.core.storage.fs.FSUtils;
import org.roda.core.util.CommandException;
import org.roda.core.util.CommandUtility;
@@ -153,99 +157,79 @@ private Map getDisposalConfirmationExtra(DisposalConfirmationFor
return data;
}
- public StreamResponse createDisposalConfirmationReport(String confirmationId, boolean isToPrint)
- throws RODAException, IOException {
- String jqCommandTemplate = RodaCoreFactory.getRodaConfigurationAsString(DISPOSAL_CONFIRMATION_COMMAND_PROPERTY);
-
- Path metadataPath = getDisposalConfirmationMetadataPath(confirmationId);
- Path aipsPath = getDisposalConfirmationAIPsPath(confirmationId);
- Path schedulesPath = getDisposalConfirmationSchedulesPath(confirmationId);
- Path holdsPath = getDisposalConfirmationHoldsPath(confirmationId);
-
- Map values = new HashMap<>();
-
- values.put(METADATA_FILE_PLACEHOLDER, metadataPath.toString());
- values.put(AIPS_FILE_PLACEHOLDER, aipsPath.toString());
- values.put(SCHEDULES_FILE_PLACEHOLDER, schedulesPath.toString());
- values.put(HOLDS_FILE_PLACEHOLDER, holdsPath.toString());
-
- String jqCommandParams = HandlebarsUtility.executeHandlebars(jqCommandTemplate, values);
-
- List jqCommand = new ArrayList<>();
- Collections.addAll(jqCommand, jqCommandParams.split(" "));
-
- String output;
- try {
- output = CommandUtility.execute(jqCommand);
- } catch (CommandException e) {
- throw new RODAException(e);
- }
- TypeReference