ProjectRequirementVersion.java
package com.taxonomy.portfolio.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.Instant;
import java.util.Objects;
/** Immutable text/provenance version of a project requirement in one exact tenant. */
@Entity
@Table(name = "project_req_version", indexes = {
@Index(name = "idx_reqver_req", columnList = "scope_key,requirement_id"),
@Index(name = "idx_reqver_scope", columnList = "scope_key")
}, uniqueConstraints = {
@UniqueConstraint(name = "uq_reqver_number",
columnNames = {"scope_key", "requirement_id", "version_number"}),
@UniqueConstraint(name = "uq_reqver_hash",
columnNames = {"scope_key", "requirement_id", "content_hash"}),
@UniqueConstraint(name = "uq_reqver_id_scope", columnNames = {"id", "scope_key"}),
@UniqueConstraint(name = "uq_reqver_id_req_scope",
columnNames = {"id", "requirement_id", "scope_key"})
})
public class ProjectRequirementVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "scope_key", nullable = false,
length = PortfolioTenantIdentity.MAX_SCOPE_KEY_LENGTH)
private String scopeKey;
/** Read-only scalar parent identity for scoped queries without initializing the association. */
@Column(name = "requirement_id", nullable = false, insertable = false, updatable = false)
private Long requirementId;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "requirement_id", nullable = false)
private ProjectRequirement requirement;
@Column(name = "version_number", nullable = false)
private int versionNumber;
@Lob
@Column(name = "requirement_text", nullable = false)
private String text;
@Column(name = "content_hash", nullable = false, length = 64)
private String contentHash;
@Column(name = "change_reason", length = 1000)
private String changeReason;
@Column(name = "created_by", nullable = false, length = 160)
private String createdBy;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "source_artifact_id")
private Long sourceArtifactId;
@Column(name = "source_version_id")
private Long sourceVersionId;
/** JSON array of source-fragment database IDs. */
@Lob
@Column(name = "source_fragment_ids")
private String sourceFragmentIdsJson;
@Column(name = "section_ref", length = 500)
private String sectionReference;
@Column(name = "page_number")
private Integer pageNumber;
@Lob
@Column(name = "original_text")
private String originalText;
protected ProjectRequirementVersion() {
}
public ProjectRequirementVersion(ProjectRequirement requirement,
int versionNumber,
String text,
String contentHash,
String changeReason,
String createdBy,
Instant createdAt,
Long sourceArtifactId,
Long sourceVersionId,
String sourceFragmentIdsJson,
String sectionReference,
Integer pageNumber,
String originalText) {
this.requirement = requirement;
this.versionNumber = versionNumber;
this.text = text;
this.contentHash = contentHash;
this.changeReason = changeReason;
this.createdBy = createdBy;
this.createdAt = createdAt;
this.sourceArtifactId = sourceArtifactId;
this.sourceVersionId = sourceVersionId;
this.sourceFragmentIdsJson = sourceFragmentIdsJson;
this.sectionReference = sectionReference;
this.pageNumber = pageNumber;
this.originalText = originalText;
synchronizeTenantAuthority(false);
}
@PrePersist
@PreUpdate
private void synchronizeTenantAuthority() {
synchronizeTenantAuthority(true);
}
private void synchronizeTenantAuthority(boolean requirePersistentParent) {
if (requirement == null || requirement.getScopeKey() == null
|| requirement.getScopeKey().isBlank()) {
throw new IllegalArgumentException(
"Requirement version must reference an exact requirement tenant");
}
String requirementScope = requirement.getScopeKey().strip();
if (scopeKey != null && !scopeKey.isBlank()
&& !requirementScope.equals(scopeKey.strip())) {
throw new IllegalArgumentException(
"Requirement version tenant scope does not match its requirement");
}
scopeKey = requirementScope;
Long persistentRequirementId = requirement.getId();
if (persistentRequirementId == null) {
if (requirePersistentParent) {
throw new IllegalArgumentException(
"Requirement must be persisted before its version");
}
return;
}
if (requirementId != null && !Objects.equals(requirementId, persistentRequirementId)) {
throw new IllegalArgumentException(
"Requirement version parent ID does not match its association");
}
requirementId = persistentRequirementId;
}
public Long getId() { return id; }
public String getScopeKey() { return scopeKey; }
public Long getRequirementId() { return requirementId; }
public ProjectRequirement getRequirement() { return requirement; }
public int getVersionNumber() { return versionNumber; }
public String getText() { return text; }
public String getContentHash() { return contentHash; }
public String getChangeReason() { return changeReason; }
public String getCreatedBy() { return createdBy; }
public Instant getCreatedAt() { return createdAt; }
public Long getSourceArtifactId() { return sourceArtifactId; }
public Long getSourceVersionId() { return sourceVersionId; }
public String getSourceFragmentIdsJson() { return sourceFragmentIdsJson; }
public String getSectionReference() { return sectionReference; }
public Integer getPageNumber() { return pageNumber; }
public String getOriginalText() { return originalText; }
}