RequirementRelationMapping.java

package com.taxonomy.portfolio.model;

import com.taxonomy.portfolio.model.PortfolioTypes.ReviewStatus;
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;

/** Queryable relation contained in an immutable exact-tenant analysis snapshot. */
@Entity
@Table(name = "req_relation_mapping", indexes = {
        @Index(name = "idx_relmap_snapshot", columnList = "scope_key,snapshot_id"),
        @Index(name = "idx_relmap_source", columnList = "scope_key,source_code"),
        @Index(name = "idx_relmap_target", columnList = "scope_key,target_code"),
        @Index(name = "idx_relmap_scope", columnList = "scope_key")
}, uniqueConstraints = {
        @UniqueConstraint(name = "uq_relmap_signature",
                columnNames = {"scope_key", "snapshot_id", "source_code", "target_code",
                        "relation_type", "relation_origin"}),
        @UniqueConstraint(name = "uq_relmap_id_scope", columnNames = {"id", "scope_key"})
})
public class RequirementRelationMapping {

    @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 = "snapshot_id", nullable = false)
    private String snapshotId;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumns({
            @JoinColumn(name = "snapshot_id", referencedColumnName = "id",
                    nullable = false, insertable = false, updatable = false),
            @JoinColumn(name = "scope_key", referencedColumnName = "scope_key",
                    nullable = false, insertable = false, updatable = false)
    })
    private RequirementAnalysisSnapshot snapshot;

    @Column(name = "source_code", nullable = false, length = 80)
    private String sourceCode;

    @Column(name = "target_code", nullable = false, length = 80)
    private String targetCode;

    @Column(name = "relation_type", nullable = false, length = 64)
    private String relationType;

    @Column(name = "relation_origin", nullable = false, length = 64)
    private String relationOrigin;

    @Column(name = "relation_category", length = 32)
    private String relationCategory;

    @Column(nullable = false)
    private double relevance;

    @Column(nullable = false)
    private double confidence;

    @Column(name = "presence_reason", length = 2000)
    private String presenceReason;

    @Enumerated(EnumType.STRING)
    @Column(name = "review_status", nullable = false, length = 32)
    private ReviewStatus reviewStatus = ReviewStatus.PROPOSED;

    @Column(name = "decision_by", length = 160)
    private String decisionBy;

    @Column(name = "decision_at")
    private Instant decisionAt;

    @Column(name = "decision_comment", length = 2000)
    private String decisionComment;

    @Version
    @Column(name = "row_version", nullable = false)
    private long rowVersion;

    protected RequirementRelationMapping() {
    }

    public RequirementRelationMapping(RequirementAnalysisSnapshot snapshot,
                                      String sourceCode,
                                      String targetCode,
                                      String relationType,
                                      String relationOrigin,
                                      String relationCategory,
                                      double relevance,
                                      double confidence,
                                      String presenceReason) {
        this.snapshot = snapshot;
        this.sourceCode = sourceCode;
        this.targetCode = targetCode;
        this.relationType = relationType;
        this.relationOrigin = relationOrigin;
        this.relationCategory = relationCategory;
        this.relevance = relevance;
        this.confidence = confidence;
        this.presenceReason = presenceReason;
        synchronizeTenantAuthority(false);
    }

    public void review(ReviewStatus status, String user, String comment, Instant now) {
        if (status != null) this.reviewStatus = status;
        this.decisionBy = user;
        this.decisionComment = comment;
        this.decisionAt = now;
    }

    @PrePersist
    @PreUpdate
    private void synchronizeTenantAuthority() {
        synchronizeTenantAuthority(true);
    }

    private void synchronizeTenantAuthority(boolean requirePersistentParent) {
        if (snapshot == null || snapshot.getScopeKey() == null
                || snapshot.getScopeKey().isBlank()) {
            throw new IllegalArgumentException(
                    "Relation mapping snapshot must expose an exact tenant scope");
        }
        String snapshotScope = snapshot.getScopeKey().strip();
        if (scopeKey != null && !scopeKey.isBlank()
                && !snapshotScope.equals(scopeKey.strip())) {
            throw new IllegalArgumentException(
                    "Relation mapping tenant scope does not match its snapshot");
        }
        scopeKey = snapshotScope;
        if (snapshot.getId() == null) {
            if (requirePersistentParent) {
                throw new IllegalArgumentException(
                        "Relation mapping snapshot must be persisted before the mapping");
            }
            return;
        }
        if (snapshotId != null && !Objects.equals(snapshotId, snapshot.getId())) {
            throw new IllegalArgumentException(
                    "Relation mapping snapshot ID does not match its association");
        }
        snapshotId = snapshot.getId();
    }

    public Long getId() { return id; }
    public String getScopeKey() { return scopeKey; }
    public String getSnapshotId() { return snapshotId; }
    public RequirementAnalysisSnapshot getSnapshot() { return snapshot; }
    public String getSourceCode() { return sourceCode; }
    public String getTargetCode() { return targetCode; }
    public String getRelationType() { return relationType; }
    public String getRelationOrigin() { return relationOrigin; }
    public String getRelationCategory() { return relationCategory; }
    public double getRelevance() { return relevance; }
    public double getConfidence() { return confidence; }
    public String getPresenceReason() { return presenceReason; }
    public ReviewStatus getReviewStatus() { return reviewStatus; }
    public String getDecisionBy() { return decisionBy; }
    public Instant getDecisionAt() { return decisionAt; }
    public String getDecisionComment() { return decisionComment; }
    public long getRowVersion() { return rowVersion; }
}