RequirementElementMapping.java

package com.taxonomy.portfolio.model;

import com.taxonomy.portfolio.model.PortfolioTypes.ActionStatus;
import com.taxonomy.portfolio.model.PortfolioTypes.MappingOrigin;
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 exact-tenant requirement-snapshot to taxonomy-element mapping. */
@Entity
@Table(name = "req_element_mapping", indexes = {
        @Index(name = "idx_elmap_snapshot", columnList = "scope_key,snapshot_id"),
        @Index(name = "idx_elmap_node", columnList = "scope_key,node_code"),
        @Index(name = "idx_elmap_action", columnList = "scope_key,action_status"),
        @Index(name = "idx_elmap_scope", columnList = "scope_key")
}, uniqueConstraints = {
        @UniqueConstraint(name = "uq_elmap_snap_node",
                columnNames = {"scope_key", "snapshot_id", "node_code"}),
        @UniqueConstraint(name = "uq_elmap_id_scope", columnNames = {"id", "scope_key"})
})
public class RequirementElementMapping {

    @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 = "node_code", nullable = false, length = 80)
    private String nodeCode;

    @Column(name = "node_title", length = 500)
    private String nodeTitle;

    @Column(name = "taxonomy_root", nullable = false, length = 8)
    private String taxonomyRoot;

    @Column(name = "direct_score", nullable = false)
    private int directScore;

    @Column(nullable = false)
    private double relevance;

    @Column(nullable = false)
    private double confidence;

    @Enumerated(EnumType.STRING)
    @Column(name = "mapping_origin", nullable = false, length = 32)
    private MappingOrigin mappingOrigin;

    @Column(name = "hierarchy_path", length = 2000)
    private String hierarchyPath;

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

    @Column(name = "selected_for_impact", nullable = false)
    private boolean selectedForImpact;

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

    @Enumerated(EnumType.STRING)
    @Column(name = "action_status", nullable = false, length = 32)
    private ActionStatus actionStatus = ActionStatus.UNDECIDED;

    @Column(name = "action_evidence", length = 2000)
    private String actionEvidence;

    @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 RequirementElementMapping() {
    }

    public RequirementElementMapping(RequirementAnalysisSnapshot snapshot,
                                     String nodeCode,
                                     String nodeTitle,
                                     String taxonomyRoot,
                                     int directScore,
                                     double relevance,
                                     double confidence,
                                     MappingOrigin mappingOrigin,
                                     String hierarchyPath,
                                     String presenceReason,
                                     boolean selectedForImpact) {
        this.snapshot = snapshot;
        this.nodeCode = nodeCode;
        this.nodeTitle = nodeTitle;
        this.taxonomyRoot = taxonomyRoot;
        this.directScore = directScore;
        this.relevance = relevance;
        this.confidence = confidence;
        this.mappingOrigin = mappingOrigin;
        this.hierarchyPath = hierarchyPath;
        this.presenceReason = presenceReason;
        this.selectedForImpact = selectedForImpact;
        synchronizeTenantAuthority(false);
    }

    public void review(ReviewStatus reviewStatus,
                       ActionStatus actionStatus,
                       String actionEvidence,
                       String decisionBy,
                       String decisionComment,
                       Instant decisionAt) {
        if (reviewStatus != null) this.reviewStatus = reviewStatus;
        if (actionStatus != null) this.actionStatus = actionStatus;
        this.actionEvidence = actionEvidence;
        this.decisionBy = decisionBy;
        this.decisionComment = decisionComment;
        this.decisionAt = decisionAt;
    }

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

    private void synchronizeTenantAuthority(boolean requirePersistentParent) {
        if (snapshot == null || snapshot.getScopeKey() == null
                || snapshot.getScopeKey().isBlank()) {
            throw new IllegalArgumentException(
                    "Element 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(
                    "Element mapping tenant scope does not match its snapshot");
        }
        scopeKey = snapshotScope;
        if (snapshot.getId() == null) {
            if (requirePersistentParent) {
                throw new IllegalArgumentException(
                        "Element mapping snapshot must be persisted before the mapping");
            }
            return;
        }
        if (snapshotId != null && !Objects.equals(snapshotId, snapshot.getId())) {
            throw new IllegalArgumentException(
                    "Element 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 getNodeCode() { return nodeCode; }
    public String getNodeTitle() { return nodeTitle; }
    public String getTaxonomyRoot() { return taxonomyRoot; }
    public int getDirectScore() { return directScore; }
    public double getRelevance() { return relevance; }
    public double getConfidence() { return confidence; }
    public MappingOrigin getMappingOrigin() { return mappingOrigin; }
    public String getHierarchyPath() { return hierarchyPath; }
    public String getPresenceReason() { return presenceReason; }
    public boolean isSelectedForImpact() { return selectedForImpact; }
    public ReviewStatus getReviewStatus() { return reviewStatus; }
    public ActionStatus getActionStatus() { return actionStatus; }
    public String getActionEvidence() { return actionEvidence; }
    public String getDecisionBy() { return decisionBy; }
    public Instant getDecisionAt() { return decisionAt; }
    public String getDecisionComment() { return decisionComment; }
    public long getRowVersion() { return rowVersion; }
}