diff --git a/roda-common/roda-common-data/pom.xml b/roda-common/roda-common-data/pom.xml
index eda2db2529..294b733b43 100644
--- a/roda-common/roda-common-data/pom.xml
+++ b/roda-common/roda-common-data/pom.xml
@@ -67,6 +67,10 @@
org.apache.commons
commons-lang3
+
+ jakarta.persistence
+ jakarta.persistence-api
+
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Job.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Job.java
index 1b61b1dd21..538a030a1f 100644
--- a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Job.java
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Job.java
@@ -22,14 +22,32 @@
import org.roda.core.data.v2.ip.HasId;
import org.roda.core.data.v2.ip.HasInstanceID;
import org.roda.core.data.v2.ip.HasInstanceName;
+import org.roda.core.data.v2.jpa.JobUserDetailsListConverter;
+import org.roda.core.data.v2.jpa.ObjectMapConverter;
+import org.roda.core.data.v2.jpa.SelectedItemsConverter;
+import org.roda.core.data.v2.jpa.StringListConverter;
+import org.roda.core.data.v2.jpa.StringMapConverter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
+import jakarta.persistence.Transient;
+
/**
* @author Hélder Silva
*/
+@Entity
+@Table(name = "jobs")
@jakarta.xml.bind.annotation.XmlRootElement(name = RodaConstants.RODA_OBJECT_JOB)
@JsonInclude(JsonInclude.Include.ALWAYS)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -42,46 +60,98 @@ public enum JOB_STATE {
PENDING_APPROVAL, REJECTED, SCHEDULED;
}
// job identifier
+ @Id
+ @Column(name = "id")
private String id = null;
// job name
+ @Column(name = "name")
private String name = null;
// job creator
+ @Column(name = "username")
private String username = null;
// job start date
+ @Column(name = "start_date")
+ @Temporal(TemporalType.TIMESTAMP)
private Date startDate = null;
// job end date
+ @Column(name = "end_date")
+ @Temporal(TemporalType.TIMESTAMP)
private Date endDate = null;
// job state
+ @Enumerated(EnumType.STRING)
+ @Column(name = "state")
private JOB_STATE state = null;
// job state details
+ @Column(name = "state_details", columnDefinition = "TEXT")
private String stateDetails = "";
// job instance id
+ @Column(name = "instance_id")
private String instanceId = null;
+ @Column(name = "job_users_details", columnDefinition = "TEXT")
+ @Convert(converter = JobUserDetailsListConverter.class)
private List jobUsersDetails = new ArrayList<>();
+ @Column(name = "instance_name")
private String instanceName = null;
- // job statistics (total source objects, etc.)
- JobStats jobStats = new JobStats();
+ // job statistics - expanded into separate columns
+ @Column(name = "stats_completion_percentage")
+ private int statsCompletionPercentage = 0;
+ @Column(name = "stats_source_objects_count")
+ private int statsSourceObjectsCount = 0;
+ @Column(name = "stats_source_objects_being_processed")
+ private int statsSourceObjectsBeingProcessed = 0;
+ @Column(name = "stats_source_objects_waiting_to_be_processed")
+ private int statsSourceObjectsWaitingToBeProcessed = 0;
+ @Column(name = "stats_source_objects_processed_with_success")
+ private int statsSourceObjectsProcessedWithSuccess = 0;
+ @Column(name = "stats_source_objects_processed_with_partial_success")
+ private int statsSourceObjectsProcessedWithPartialSuccess = 0;
+ @Column(name = "stats_source_objects_processed_with_failure")
+ private int statsSourceObjectsProcessedWithFailure = 0;
+ @Column(name = "stats_source_objects_processed_with_skipped")
+ private int statsSourceObjectsProcessedWithSkipped = 0;
+ @Column(name = "stats_outcome_objects_with_manual_intervention")
+ private int statsOutcomeObjectsWithManualIntervention = 0;
+
+ // Transient jobStats field for backwards compatibility with existing code
+ @Transient
+ private JobStats jobStats = new JobStats();
// plugin full class (e.g. org.roda.core.plugins.plugins.base.FixityPlugin)
+ @Column(name = "plugin")
private String plugin = null;
// plugin type (e.g. ingest, maintenance, misc, etc.)
+ @Enumerated(EnumType.STRING)
+ @Column(name = "plugin_type")
private PluginType pluginType = null;
// plugin parameters
+ @Column(name = "plugin_parameters", columnDefinition = "TEXT")
+ @Convert(converter = StringMapConverter.class)
private Map pluginParameters = new HashMap<>();
// objects to act upon (All, None, List, Filter, etc.)
+ @Column(name = "source_objects", columnDefinition = "TEXT")
+ @Convert(converter = SelectedItemsConverter.class)
private SelectedItems extends IsRODAObject> sourceObjects = null;
+ @Column(name = "outcome_objects_class")
private String outcomeObjectsClass = "";
+ @Column(name = "attachments_list", columnDefinition = "TEXT")
+ @Convert(converter = StringListConverter.class)
private List attachmentsList = new ArrayList<>();
+ @Column(name = "fields", columnDefinition = "TEXT")
+ @Convert(converter = ObjectMapConverter.class)
private Map fields;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "priority")
private JobPriority priority;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "parallelism")
private JobParallelism parallelism;
public Job() {
@@ -104,7 +174,7 @@ public Job(Job job) {
this.pluginParameters = new HashMap<>(job.getPluginParameters());
this.sourceObjects = job.getSourceObjects();
if (sourceObjects instanceof SelectedItemsList) {
- jobStats.setSourceObjectsCount(((SelectedItemsList>) sourceObjects).getIds().size());
+ getJobStats().setSourceObjectsCount(((SelectedItemsList>) sourceObjects).getIds().size());
}
this.instanceId = job.getInstanceId();
this.instanceName = job.getInstanceName();
@@ -112,6 +182,7 @@ public Job(Job job) {
this.jobUsersDetails = job.getJobUsersDetails();
}
+ @Transient
@JsonIgnore
@Override
public int getClassVersion() {
@@ -205,11 +276,36 @@ public void setStateDetails(String stateDetails) {
}
public JobStats getJobStats() {
+ // Build JobStats from individual columns for backwards compatibility
+ if (jobStats == null) {
+ jobStats = new JobStats();
+ }
+ jobStats.setCompletionPercentage(statsCompletionPercentage);
+ jobStats.setSourceObjectsCount(statsSourceObjectsCount);
+ jobStats.setSourceObjectsBeingProcessed(statsSourceObjectsBeingProcessed);
+ jobStats.setSourceObjectsWaitingToBeProcessed(statsSourceObjectsWaitingToBeProcessed);
+ jobStats.setSourceObjectsProcessedWithSuccess(statsSourceObjectsProcessedWithSuccess);
+ jobStats.setSourceObjectsProcessedWithPartialSuccess(statsSourceObjectsProcessedWithPartialSuccess);
+ jobStats.setSourceObjectsProcessedWithFailure(statsSourceObjectsProcessedWithFailure);
+ jobStats.setSourceObjectsProcessedWithSkipped(statsSourceObjectsProcessedWithSkipped);
+ jobStats.setOutcomeObjectsWithManualIntervention(statsOutcomeObjectsWithManualIntervention);
return jobStats;
}
public void setJobStats(JobStats jobStats) {
this.jobStats = jobStats;
+ // Sync to individual columns
+ if (jobStats != null) {
+ this.statsCompletionPercentage = jobStats.getCompletionPercentage();
+ this.statsSourceObjectsCount = jobStats.getSourceObjectsCount();
+ this.statsSourceObjectsBeingProcessed = jobStats.getSourceObjectsBeingProcessed();
+ this.statsSourceObjectsWaitingToBeProcessed = jobStats.getSourceObjectsWaitingToBeProcessed();
+ this.statsSourceObjectsProcessedWithSuccess = jobStats.getSourceObjectsProcessedWithSuccess();
+ this.statsSourceObjectsProcessedWithPartialSuccess = jobStats.getSourceObjectsProcessedWithPartialSuccess();
+ this.statsSourceObjectsProcessedWithFailure = jobStats.getSourceObjectsProcessedWithFailure();
+ this.statsSourceObjectsProcessedWithSkipped = jobStats.getSourceObjectsProcessedWithSkipped();
+ this.statsOutcomeObjectsWithManualIntervention = jobStats.getOutcomeObjectsWithManualIntervention();
+ }
}
public String getPlugin() {
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Report.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Report.java
index b8197e7f92..02ca3a29d1 100644
--- a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Report.java
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/Report.java
@@ -17,11 +17,30 @@
import org.roda.core.data.v2.ip.HasId;
import org.roda.core.data.v2.ip.HasInstanceID;
import org.roda.core.data.v2.ip.SIPInformation;
+import org.roda.core.data.v2.jpa.StringListConverter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OrderBy;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
+import jakarta.persistence.Transient;
+
+@Entity
+@Table(name = "job_reports", indexes = {@Index(name = "idx_report_job_id", columnList = "jobId")})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Report implements IsModelObject, HasId, HasInstanceID {
@Serial
@@ -32,39 +51,78 @@ public class Report implements IsModelObject, HasId, HasInstanceID {
public static final String NO_OUTCOME_OBJECT_ID = "NO_OUTCOME_ID";
public static final String NO_OUTCOME_OBJECT_CLASS = "NO_OUTCOME_CLASS";
+ @Id
+ @Column(name = "id")
private String id = "";
+ @Column(name = "job_id")
private String jobId = "";
+ @Column(name = "source_object_id")
private String sourceObjectId = NO_SOURCE_OBJECT_ID;
+ @Column(name = "source_object_class")
private String sourceObjectClass = NO_SOURCE_OBJECT_CLASS;
+ @Column(name = "source_object_original_ids", columnDefinition = "TEXT")
+ @Convert(converter = StringListConverter.class)
private List sourceObjectOriginalIds = new ArrayList<>();
+ @Column(name = "source_object_original_name")
private String sourceObjectOriginalName = "";
+ @Column(name = "outcome_object_id")
private String outcomeObjectId = NO_OUTCOME_OBJECT_ID;
+ @Column(name = "outcome_object_class")
private String outcomeObjectClass = NO_OUTCOME_OBJECT_CLASS;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "outcome_object_state")
private AIPState outcomeObjectState = AIPState.getDefault();
+ @Column(name = "title")
private String title = "";
+ @Column(name = "date_created")
+ @Temporal(TemporalType.TIMESTAMP)
private Date dateCreated;
+ @Column(name = "date_updated")
+ @Temporal(TemporalType.TIMESTAMP)
private Date dateUpdated;
+ @Column(name = "ingest_type")
private String ingestType = "";
+ @Column(name = "completion_percentage")
private Integer completionPercentage = 0;
+ @Column(name = "steps_completed")
private Integer stepsCompleted = 0;
+ @Column(name = "total_steps")
private Integer totalSteps = 0;
+ @Column(name = "plugin")
private String plugin = "";
+ @Column(name = "plugin_name")
private String pluginName = "";
+ @Column(name = "plugin_version")
private String pluginVersion = "";
+ @Enumerated(EnumType.STRING)
+ @Column(name = "plugin_state")
private PluginState pluginState = PluginState.RUNNING;
+ @Column(name = "plugin_is_mandatory")
private Boolean pluginIsMandatory = true;
+ @Column(name = "plugin_details", columnDefinition = "TEXT")
private String pluginDetails = "";
+ @Column(name = "html_plugin_details")
private boolean htmlPluginDetails = false;
+ @Column(name = "instance_id")
private String instanceId = null;
+ @Column(name = "transaction_id")
private String transactionId = null;
+ @Transient
@JsonIgnore
private SIPInformation sipInformation = new SIPInformation();
- private List reports = new ArrayList<>();
+ @OneToMany(mappedBy = "parentReportId", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
+ @OrderBy("stepOrder ASC")
+ private List stepReports = new ArrayList<>();
+ // Transient field for backwards compatibility with existing code that uses List
+ @Transient
+ private List reports = null;
+
+ @Column(name = "line_separator")
private String lineSeparator = "";
public Report() {
@@ -101,11 +159,13 @@ public Report(Report report) {
this.pluginIsMandatory = report.getPluginIsMandatory();
this.pluginDetails = report.getPluginDetails();
this.htmlPluginDetails = report.isHtmlPluginDetails();
- this.reports = new ArrayList<>();
+ this.reports = null;
+ this.stepReports = new ArrayList<>();
this.instanceId = report.getInstanceId();
this.transactionId = report.getTransactionId();
}
+ @Transient
@JsonIgnore
@Override
public int getClassVersion() {
@@ -420,12 +480,25 @@ public Report addReport(Report report, boolean updateReportItemDateUpdated) {
ReportUtils.calculatePluginState(getPluginState(), report.getPluginState(), report.getPluginIsMandatory()));
if (!"".equals(report.getPluginDetails()) && !getPluginDetails().equals(report.getPluginDetails())) {
+ // Fix: avoid adding repeated line separators
+ String separator = (lineSeparator != null && !lineSeparator.isEmpty()) ? lineSeparator : "\n";
setPluginDetails(
- (!"".equals(getPluginDetails()) ? getPluginDetails() + lineSeparator : "") + report.getPluginDetails());
+ (!"".equals(getPluginDetails()) ? getPluginDetails() + separator : "") + report.getPluginDetails());
}
setOutcomeObjectState(report.getOutcomeObjectState());
+ // Add to both stepReports (JPA entity) and reports (backwards compatibility)
+ if (reports == null) {
+ reports = new ArrayList<>();
+ }
reports.add(report);
+
+ // Also add as StepReport
+ if (stepReports == null) {
+ stepReports = new ArrayList<>();
+ }
+ stepReports.add(new StepReport(report, this.id, stepReports.size()));
+
return this;
}
@@ -445,15 +518,50 @@ public Report addReport(Report report, boolean updateReportItemDateUpdated) {
* return newPluginState; }
*/
+ /**
+ * Get the step reports as a list of Report objects for backwards compatibility.
+ * If reports is null, builds it from stepReports.
+ */
public List getReports() {
- return reports;
+ if (reports == null && stepReports != null) {
+ reports = new ArrayList<>();
+ for (StepReport stepReport : stepReports) {
+ reports.add(stepReport.toReport());
+ }
+ }
+ return reports != null ? reports : new ArrayList<>();
}
public Report setReports(List reports) {
this.reports = reports;
+ // Sync to stepReports
+ if (reports != null) {
+ this.stepReports = new ArrayList<>();
+ int order = 0;
+ for (Report report : reports) {
+ this.stepReports.add(new StepReport(report, this.id, order++));
+ }
+ }
return this;
}
+ /**
+ * Get the underlying StepReport entities.
+ */
+ @JsonIgnore
+ public List getStepReports() {
+ return stepReports;
+ }
+
+ /**
+ * Set the underlying StepReport entities.
+ */
+ @JsonIgnore
+ public void setStepReports(List stepReports) {
+ this.stepReports = stepReports;
+ this.reports = null; // Reset cached reports
+ }
+
@JsonProperty("lineSeparator")
public void injectLineSeparator(String lineSeparator) {
this.lineSeparator = lineSeparator;
@@ -465,10 +573,11 @@ public String getLineSeparator() {
@JsonIgnore
public Report getLastRunPlugin() {
- int size = reports.size();
+ List reportsList = getReports();
+ int size = reportsList.size();
if (size == 0)
return null;
- return reports.get(size - 1);
+ return reportsList.get(size - 1);
}
@Override
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/StepReport.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/StepReport.java
new file mode 100644
index 0000000000..12eee32243
--- /dev/null
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jobs/StepReport.java
@@ -0,0 +1,347 @@
+/**
+ * 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.data.v2.jobs;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.UUID;
+
+import org.roda.core.data.v2.ip.AIPState;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
+
+/**
+ * StepReport represents a single step/plugin execution within a job report.
+ * This is a separate JPA entity to properly store step reports in their own table
+ * with a foreign key relationship to the parent Report.
+ *
+ * @author RODA Development Team
+ */
+@Entity
+@Table(name = "job_step_reports", indexes = {@Index(name = "idx_step_report_parent_id", columnList = "parent_report_id")})
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class StepReport implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column(name = "id")
+ private String id;
+
+ @Column(name = "parent_report_id")
+ private String parentReportId;
+
+ @Column(name = "source_object_id")
+ private String sourceObjectId = Report.NO_SOURCE_OBJECT_ID;
+
+ @Column(name = "source_object_class")
+ private String sourceObjectClass = Report.NO_SOURCE_OBJECT_CLASS;
+
+ @Column(name = "outcome_object_id")
+ private String outcomeObjectId = Report.NO_OUTCOME_OBJECT_ID;
+
+ @Column(name = "outcome_object_class")
+ private String outcomeObjectClass = Report.NO_OUTCOME_OBJECT_CLASS;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "outcome_object_state")
+ private AIPState outcomeObjectState = AIPState.getDefault();
+
+ @Column(name = "title")
+ private String title = "";
+
+ @Column(name = "date_created")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date dateCreated;
+
+ @Column(name = "date_updated")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date dateUpdated;
+
+ @Column(name = "completion_percentage")
+ private Integer completionPercentage = 0;
+
+ @Column(name = "steps_completed")
+ private Integer stepsCompleted = 0;
+
+ @Column(name = "total_steps")
+ private Integer totalSteps = 0;
+
+ @Column(name = "plugin")
+ private String plugin = "";
+
+ @Column(name = "plugin_name")
+ private String pluginName = "";
+
+ @Column(name = "plugin_version")
+ private String pluginVersion = "";
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "plugin_state")
+ private PluginState pluginState = PluginState.RUNNING;
+
+ @Column(name = "plugin_is_mandatory")
+ private Boolean pluginIsMandatory = true;
+
+ @Column(name = "plugin_details", columnDefinition = "TEXT")
+ private String pluginDetails = "";
+
+ @Column(name = "html_plugin_details")
+ private boolean htmlPluginDetails = false;
+
+ @Column(name = "step_order")
+ private Integer stepOrder = 0;
+
+ public StepReport() {
+ super();
+ this.id = UUID.randomUUID().toString();
+ this.dateCreated = new Date();
+ }
+
+ /**
+ * Create a StepReport from a Report (for backwards compatibility during migration)
+ */
+ public StepReport(Report report, String parentReportId, int stepOrder) {
+ this();
+ this.parentReportId = parentReportId;
+ this.stepOrder = stepOrder;
+ this.sourceObjectId = report.getSourceObjectId();
+ this.sourceObjectClass = report.getSourceObjectClass();
+ this.outcomeObjectId = report.getOutcomeObjectId();
+ this.outcomeObjectClass = report.getOutcomeObjectClass();
+ this.outcomeObjectState = report.getOutcomeObjectState();
+ this.title = report.getTitle();
+ this.dateCreated = report.getDateCreated();
+ this.dateUpdated = report.getDateUpdated();
+ this.completionPercentage = report.getCompletionPercentage();
+ this.stepsCompleted = report.getStepsCompleted();
+ this.totalSteps = report.getTotalSteps();
+ this.plugin = report.getPlugin();
+ this.pluginName = report.getPluginName();
+ this.pluginVersion = report.getPluginVersion();
+ this.pluginState = report.getPluginState();
+ this.pluginIsMandatory = report.getPluginIsMandatory();
+ this.pluginDetails = report.getPluginDetails();
+ this.htmlPluginDetails = report.isHtmlPluginDetails();
+ }
+
+ /**
+ * Convert this StepReport to a Report (for backwards compatibility)
+ */
+ @JsonIgnore
+ public Report toReport() {
+ Report report = new Report();
+ report.setId(this.id);
+ report.setSourceObjectId(this.sourceObjectId);
+ report.setSourceObjectClass(this.sourceObjectClass);
+ report.setOutcomeObjectId(this.outcomeObjectId);
+ report.setOutcomeObjectClass(this.outcomeObjectClass);
+ report.setOutcomeObjectState(this.outcomeObjectState);
+ report.setTitle(this.title);
+ report.setDateCreated(this.dateCreated);
+ report.setDateUpdated(this.dateUpdated);
+ report.setCompletionPercentage(this.completionPercentage);
+ report.setStepsCompleted(this.stepsCompleted);
+ report.setTotalSteps(this.totalSteps);
+ report.setPlugin(this.plugin);
+ report.setPluginName(this.pluginName);
+ report.setPluginVersion(this.pluginVersion);
+ report.setPluginState(this.pluginState);
+ report.setPluginIsMandatory(this.pluginIsMandatory);
+ report.setPluginDetails(this.pluginDetails);
+ report.setHtmlPluginDetails(this.htmlPluginDetails);
+ return report;
+ }
+
+ // Getters and setters
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getParentReportId() {
+ return parentReportId;
+ }
+
+ public void setParentReportId(String parentReportId) {
+ this.parentReportId = parentReportId;
+ }
+
+ public String getSourceObjectId() {
+ return sourceObjectId;
+ }
+
+ public void setSourceObjectId(String sourceObjectId) {
+ this.sourceObjectId = sourceObjectId;
+ }
+
+ public String getSourceObjectClass() {
+ return sourceObjectClass;
+ }
+
+ public void setSourceObjectClass(String sourceObjectClass) {
+ this.sourceObjectClass = sourceObjectClass;
+ }
+
+ public String getOutcomeObjectId() {
+ return outcomeObjectId;
+ }
+
+ public void setOutcomeObjectId(String outcomeObjectId) {
+ this.outcomeObjectId = outcomeObjectId;
+ }
+
+ public String getOutcomeObjectClass() {
+ return outcomeObjectClass;
+ }
+
+ public void setOutcomeObjectClass(String outcomeObjectClass) {
+ this.outcomeObjectClass = outcomeObjectClass;
+ }
+
+ public AIPState getOutcomeObjectState() {
+ return outcomeObjectState;
+ }
+
+ public void setOutcomeObjectState(AIPState outcomeObjectState) {
+ this.outcomeObjectState = outcomeObjectState;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public Date getDateCreated() {
+ return dateCreated;
+ }
+
+ public void setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public Date getDateUpdated() {
+ return dateUpdated;
+ }
+
+ public void setDateUpdated(Date dateUpdated) {
+ this.dateUpdated = dateUpdated;
+ }
+
+ public Integer getCompletionPercentage() {
+ return completionPercentage;
+ }
+
+ public void setCompletionPercentage(Integer completionPercentage) {
+ this.completionPercentage = completionPercentage;
+ }
+
+ public Integer getStepsCompleted() {
+ return stepsCompleted;
+ }
+
+ public void setStepsCompleted(Integer stepsCompleted) {
+ this.stepsCompleted = stepsCompleted;
+ }
+
+ public Integer getTotalSteps() {
+ return totalSteps;
+ }
+
+ public void setTotalSteps(Integer totalSteps) {
+ this.totalSteps = totalSteps;
+ }
+
+ public String getPlugin() {
+ return plugin;
+ }
+
+ public void setPlugin(String plugin) {
+ this.plugin = plugin;
+ }
+
+ public String getPluginName() {
+ return pluginName;
+ }
+
+ public void setPluginName(String pluginName) {
+ this.pluginName = pluginName;
+ }
+
+ public String getPluginVersion() {
+ return pluginVersion;
+ }
+
+ public void setPluginVersion(String pluginVersion) {
+ this.pluginVersion = pluginVersion;
+ }
+
+ public PluginState getPluginState() {
+ return pluginState;
+ }
+
+ public void setPluginState(PluginState pluginState) {
+ this.pluginState = pluginState;
+ }
+
+ public Boolean getPluginIsMandatory() {
+ return pluginIsMandatory;
+ }
+
+ public void setPluginIsMandatory(Boolean pluginIsMandatory) {
+ this.pluginIsMandatory = pluginIsMandatory;
+ }
+
+ public String getPluginDetails() {
+ return pluginDetails;
+ }
+
+ public void setPluginDetails(String pluginDetails) {
+ this.pluginDetails = pluginDetails;
+ }
+
+ public boolean isHtmlPluginDetails() {
+ return htmlPluginDetails;
+ }
+
+ public void setHtmlPluginDetails(boolean htmlPluginDetails) {
+ this.htmlPluginDetails = htmlPluginDetails;
+ }
+
+ public Integer getStepOrder() {
+ return stepOrder;
+ }
+
+ public void setStepOrder(Integer stepOrder) {
+ this.stepOrder = stepOrder;
+ }
+
+ @Override
+ public String toString() {
+ return "StepReport [id=" + id + ", parentReportId=" + parentReportId + ", sourceObjectId=" + sourceObjectId
+ + ", plugin=" + plugin + ", pluginState=" + pluginState + ", stepOrder=" + stepOrder + "]";
+ }
+}
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobStatsConverter.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobStatsConverter.java
new file mode 100644
index 0000000000..61424cd06c
--- /dev/null
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobStatsConverter.java
@@ -0,0 +1,24 @@
+/**
+ * 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.data.v2.jpa;
+
+import org.roda.core.data.v2.jobs.JobStats;
+
+import jakarta.persistence.Converter;
+
+/**
+ * JPA converter for JobStats objects.
+ */
+@Converter
+public class JobStatsConverter extends JsonAttributeConverter {
+
+ @Override
+ protected Class getAttributeClass() {
+ return JobStats.class;
+ }
+}
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobUserDetailsListConverter.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobUserDetailsListConverter.java
new file mode 100644
index 0000000000..f5f9cc5693
--- /dev/null
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JobUserDetailsListConverter.java
@@ -0,0 +1,50 @@
+/**
+ * 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.data.v2.jpa;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.roda.core.data.exceptions.GenericException;
+import org.roda.core.data.utils.JsonUtils;
+import org.roda.core.data.v2.jobs.JobUserDetails;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+
+/**
+ * JPA converter for List of JobUserDetails objects.
+ */
+@Converter
+public class JobUserDetailsListConverter implements AttributeConverter, String> {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JobUserDetailsListConverter.class);
+
+ @Override
+ public String convertToDatabaseColumn(List attribute) {
+ if (attribute == null) {
+ return null;
+ }
+ return JsonUtils.getJsonFromObject(attribute);
+ }
+
+ @Override
+ public List convertToEntityAttribute(String dbData) {
+ if (dbData == null || dbData.isEmpty()) {
+ return new ArrayList<>();
+ }
+ try {
+ return JsonUtils.getListFromJson(dbData, JobUserDetails.class);
+ } catch (GenericException e) {
+ LOGGER.error("Error converting JSON to List: {}", e.getMessage(), e);
+ return new ArrayList<>();
+ }
+ }
+}
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JsonAttributeConverter.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JsonAttributeConverter.java
new file mode 100644
index 0000000000..a1bb3e0537
--- /dev/null
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/JsonAttributeConverter.java
@@ -0,0 +1,55 @@
+/**
+ * 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.data.v2.jpa;
+
+import org.roda.core.data.exceptions.GenericException;
+import org.roda.core.data.utils.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.persistence.AttributeConverter;
+
+/**
+ * Abstract base class for JPA attribute converters that serialize/deserialize
+ * objects to/from JSON strings.
+ *
+ * @param
+ * The type of the attribute to convert
+ */
+public abstract class JsonAttributeConverter implements AttributeConverter {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JsonAttributeConverter.class);
+
+ /**
+ * Returns the Class type for deserialization.
+ *
+ * @return the class type of the attribute
+ */
+ protected abstract Class getAttributeClass();
+
+ @Override
+ public String convertToDatabaseColumn(T attribute) {
+ if (attribute == null) {
+ return null;
+ }
+ return JsonUtils.getJsonFromObject(attribute);
+ }
+
+ @Override
+ public T convertToEntityAttribute(String dbData) {
+ if (dbData == null || dbData.isEmpty()) {
+ return null;
+ }
+ try {
+ return JsonUtils.getObjectFromJson(dbData, getAttributeClass());
+ } catch (GenericException e) {
+ LOGGER.error("Error converting JSON to {}: {}", getAttributeClass().getSimpleName(), e.getMessage(), e);
+ return null;
+ }
+ }
+}
diff --git a/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/ObjectMapConverter.java b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/ObjectMapConverter.java
new file mode 100644
index 0000000000..c82cb521ee
--- /dev/null
+++ b/roda-common/roda-common-data/src/main/java/org/roda/core/data/v2/jpa/ObjectMapConverter.java
@@ -0,0 +1,54 @@
+/**
+ * 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.data.v2.jpa;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.roda.core.data.exceptions.GenericException;
+import org.roda.core.data.utils.JsonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+
+/**
+ * JPA converter for Map<String, Object> objects.
+ */
+@Converter
+public class ObjectMapConverter implements AttributeConverter