RequirementAnalysisJobItem.java
package com.taxonomy.portfolio.model;
import com.taxonomy.portfolio.model.PortfolioTypes.AnalysisStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinColumns;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.time.Instant;
import java.util.Objects;
/** Status and retry boundary for one exact-tenant requirement inside an analysis job. */
@Entity
@Table(name = "req_analysis_item", indexes = {
@Index(name = "idx_item_job", columnList = "scope_key,project_id,job_id"),
@Index(name = "idx_item_req", columnList = "scope_key,project_id,requirement_id"),
@Index(name = "idx_item_scope", columnList = "scope_key")
}, uniqueConstraints = {
@UniqueConstraint(name = "uq_item_job_req",
columnNames = {"scope_key", "job_id", "requirement_id"}),
@UniqueConstraint(name = "uq_item_id_scope", columnNames = {"id", "scope_key"})
})
public class RequirementAnalysisJobItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "scope_key", nullable = false,
length = PortfolioTenantIdentity.MAX_SCOPE_KEY_LENGTH)
private String scopeKey;
@Column(name = "project_id", nullable = false)
private Long projectId;
@Column(name = "job_id", nullable = false)
private String jobId;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumns({
@JoinColumn(name = "job_id", referencedColumnName = "id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "project_id", referencedColumnName = "project_id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "scope_key", referencedColumnName = "scope_key",
nullable = false, insertable = false, updatable = false)
})
private RequirementAnalysisJob job;
@Column(name = "requirement_id", nullable = false)
private Long requirementId;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumns({
@JoinColumn(name = "requirement_id", referencedColumnName = "id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "project_id", referencedColumnName = "project_id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "scope_key", referencedColumnName = "scope_key",
nullable = false, insertable = false, updatable = false)
})
private ProjectRequirement requirement;
@Column(name = "requirement_version_id", nullable = false)
private Long requirementVersionId;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumns({
@JoinColumn(name = "requirement_version_id", referencedColumnName = "id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "requirement_id", referencedColumnName = "requirement_id",
nullable = false, insertable = false, updatable = false),
@JoinColumn(name = "scope_key", referencedColumnName = "scope_key",
nullable = false, insertable = false, updatable = false)
})
private ProjectRequirementVersion requirementVersion;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private AnalysisStatus status = AnalysisStatus.PENDING;
@Column(name = "snapshot_id", length = 36)
private String snapshotId;
/**
* Read-only full work-identity association. A result can only point to a
* snapshot produced for this same job, project, requirement, version and tenant.
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "snapshot_id", referencedColumnName = "id",
insertable = false, updatable = false),
@JoinColumn(name = "job_id", referencedColumnName = "job_id",
insertable = false, updatable = false),
@JoinColumn(name = "requirement_id", referencedColumnName = "requirement_id",
insertable = false, updatable = false),
@JoinColumn(name = "requirement_version_id",
referencedColumnName = "requirement_version_id",
insertable = false, updatable = false),
@JoinColumn(name = "project_id", referencedColumnName = "project_id",
insertable = false, updatable = false),
@JoinColumn(name = "scope_key", referencedColumnName = "scope_key",
insertable = false, updatable = false)
})
private RequirementAnalysisSnapshot snapshot;
@Column(nullable = false)
private int attempt = 1;
@Column(name = "started_at")
private Instant startedAt;
@Column(name = "completed_at")
private Instant completedAt;
@Column(name = "error_message", length = 2000)
private String errorMessage;
@Version
@Column(name = "row_version", nullable = false)
private long rowVersion;
protected RequirementAnalysisJobItem() {
}
public RequirementAnalysisJobItem(RequirementAnalysisJob job,
ProjectRequirement requirement,
ProjectRequirementVersion requirementVersion) {
this.job = job;
this.requirement = requirement;
this.requirementVersion = requirementVersion;
synchronizeTenantAuthority(false);
}
public void markRunning(Instant now) {
this.status = AnalysisStatus.RUNNING;
this.startedAt = now;
this.completedAt = null;
this.errorMessage = null;
}
public void complete(AnalysisStatus status, String snapshotId, Instant now) {
if (status != AnalysisStatus.SUCCESS && status != AnalysisStatus.PARTIAL) {
throw new IllegalArgumentException("Completed item status must be SUCCESS or PARTIAL");
}
this.status = status;
this.snapshotId = snapshotId;
this.snapshot = null;
this.completedAt = now;
this.errorMessage = null;
}
public void fail(String errorMessage, Instant now) {
this.status = AnalysisStatus.FAILED;
this.errorMessage = errorMessage;
this.completedAt = now;
}
public void prepareRetry(ProjectRequirementVersion currentVersion) {
this.requirementVersion = currentVersion;
synchronizeTenantAuthority(true);
this.status = AnalysisStatus.PENDING;
this.snapshotId = null;
this.snapshot = null;
this.errorMessage = null;
this.startedAt = null;
this.completedAt = null;
this.attempt++;
}
@PrePersist
@PreUpdate
private void synchronizeTenantAuthority() {
synchronizeTenantAuthority(true);
}
private void synchronizeTenantAuthority(boolean requirePersistentParents) {
if (job == null || requirement == null || requirementVersion == null) {
throw new IllegalArgumentException(
"Analysis item must reference job, requirement and immutable version");
}
String jobScope = exactScope(job.getScopeKey(), "Analysis item job");
String requirementScope = exactScope(
requirement.getScopeKey(), "Analysis item requirement");
String versionScope = exactScope(
requirementVersion.getScopeKey(), "Analysis item requirement version");
if (!jobScope.equals(requirementScope) || !jobScope.equals(versionScope)) {
throw new IllegalArgumentException(
"Analysis item parents do not belong to the same tenant scope");
}
if (scopeKey != null && !scopeKey.isBlank()
&& !jobScope.equals(scopeKey.strip())) {
throw new IllegalArgumentException(
"Analysis item tenant scope does not match its parents");
}
scopeKey = jobScope;
Long jobProjectId = job.getProjectId();
Long requirementProjectId = requirement.getProjectId();
Long persistentRequirementId = requirement.getId();
Long versionRequirementId = requirementVersion.getRequirementId();
if (jobProjectId == null || requirementProjectId == null
|| persistentRequirementId == null || versionRequirementId == null
|| job.getId() == null || requirementVersion.getId() == null) {
if (requirePersistentParents) {
throw new IllegalArgumentException(
"Analysis item parents must be persisted before the item");
}
return;
}
if (!Objects.equals(jobProjectId, requirementProjectId)) {
throw new IllegalArgumentException(
"Analysis item job and requirement belong to different projects");
}
if (!Objects.equals(persistentRequirementId, versionRequirementId)) {
throw new IllegalArgumentException(
"Analysis item version belongs to another requirement");
}
if (projectId != null && !Objects.equals(projectId, jobProjectId)) {
throw new IllegalArgumentException(
"Analysis item project ID does not match its parents");
}
projectId = jobProjectId;
jobId = job.getId();
requirementId = persistentRequirementId;
requirementVersionId = requirementVersion.getId();
}
private static String exactScope(String scope, String parent) {
if (scope == null || scope.isBlank()) {
throw new IllegalArgumentException(parent + " must expose an exact tenant scope");
}
return scope.strip();
}
public Long getId() { return id; }
public String getScopeKey() { return scopeKey; }
public Long getProjectId() { return projectId; }
public String getJobId() { return jobId; }
public RequirementAnalysisJob getJob() { return job; }
public Long getRequirementId() { return requirementId; }
public ProjectRequirement getRequirement() { return requirement; }
public Long getRequirementVersionId() { return requirementVersionId; }
public ProjectRequirementVersion getRequirementVersion() { return requirementVersion; }
public AnalysisStatus getStatus() { return status; }
public String getSnapshotId() { return snapshotId; }
public RequirementAnalysisSnapshot getSnapshot() { return snapshot; }
public int getAttempt() { return attempt; }
public Instant getStartedAt() { return startedAt; }
public Instant getCompletedAt() { return completedAt; }
public String getErrorMessage() { return errorMessage; }
public long getRowVersion() { return rowVersion; }
}