RelationHypothesis.java
package com.taxonomy.relations.model;
import com.taxonomy.catalog.model.TaxonomyRelation;
import com.taxonomy.model.HypothesisStatus;
import com.taxonomy.model.RelationType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import org.hibernate.annotations.Nationalized;
import java.time.Instant;
/**
* A provisional/proposed relation hypothesis derived from analysis.
*
* <p>Unlike {@link TaxonomyRelation} which represents confirmed relations,
* a hypothesis is a candidate relation that can be accepted or rejected
* through a review workflow. Every row belongs to exactly one repository
* tenant and either its central scope or one personal workspace.</p>
*/
@Entity
@Table(name = "relation_hypothesis",
indexes = {
@Index(name = "idx_hyp_repository", columnList = "repository_id"),
@Index(name = "idx_hyp_repository_workspace",
columnList = "repository_id, workspace_id"),
@Index(name = "idx_hyp_project", columnList = "project_id"),
@Index(name = "idx_hyp_requirement", columnList = "requirement_id"),
@Index(name = "idx_hyp_snapshot", columnList = "analysis_snapshot_id")
},
uniqueConstraints = @UniqueConstraint(
name = "uq_hypothesis_repository_workspace_session_relation",
columnNames = {
"repository_id",
"workspace_scope_key",
"source_node_id",
"target_node_id",
"relation_type",
"analysis_session_scope_key"
}))
public class RelationHypothesis {
public static final String CENTRAL_SCOPE_KEY = "__shared__";
public static final String UNSPECIFIED_SESSION_SCOPE_KEY = "__unspecified__";
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "repository_id", nullable = false, length = 255)
private String repositoryId;
@Nationalized
@Column(name = "source_node_id", nullable = false)
private String sourceNodeId;
@Nationalized
@Column(name = "target_node_id", nullable = false)
private String targetNodeId;
@Enumerated(EnumType.STRING)
@Column(name = "relation_type", nullable = false)
private RelationType relationType;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private HypothesisStatus status = HypothesisStatus.PROVISIONAL;
@Column
private Double confidence;
@Nationalized
@Column(name = "analysis_session_id")
private String analysisSessionId;
/** Non-null key that makes a missing legacy analysis session deterministic. */
@Column(name = "analysis_session_scope_key", nullable = false, length = 255)
private String analysisSessionScopeKey = UNSPECIFIED_SESSION_SCOPE_KEY;
@Column(name = "project_id")
private Long projectId;
@Column(name = "requirement_id")
private Long requirementId;
@Column(name = "analysis_snapshot_id", length = 36)
private String analysisSnapshotId;
@Column(name = "applied_in_current_analysis")
private boolean appliedInCurrentAnalysis;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "workspace_id")
private String workspaceId;
/** Non-null key for central and personal-workspace uniqueness. */
@Column(name = "workspace_scope_key", nullable = false, length = 255)
private String workspaceScopeKey = CENTRAL_SCOPE_KEY;
@Column(name = "owner_username")
private String ownerUsername;
@PrePersist
protected void onCreate() {
if (createdAt == null) {
createdAt = Instant.now();
}
synchronizeTenantKeys();
}
@PreUpdate
protected void onUpdate() {
synchronizeTenantKeys();
}
private void synchronizeTenantKeys() {
repositoryId = requireText(repositoryId, "repositoryId");
sourceNodeId = requireText(sourceNodeId, "sourceNodeId");
targetNodeId = requireText(targetNodeId, "targetNodeId");
workspaceId = normalizeOptional(workspaceId);
workspaceScopeKey = scopeKeyFor(workspaceId);
analysisSessionId = normalizeOptional(analysisSessionId);
analysisSessionScopeKey = sessionScopeKeyFor(analysisSessionId);
ownerUsername = normalizeOptional(ownerUsername);
}
public static String scopeKeyFor(String workspaceId) {
String normalized = normalizeOptional(workspaceId);
return normalized == null ? CENTRAL_SCOPE_KEY : normalized;
}
public static String sessionScopeKeyFor(String analysisSessionId) {
String normalized = normalizeOptional(analysisSessionId);
return normalized == null ? UNSPECIFIED_SESSION_SCOPE_KEY : normalized;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getRepositoryId() { return repositoryId; }
public void setRepositoryId(String repositoryId) {
this.repositoryId = requireText(repositoryId, "repositoryId");
}
public String getSourceNodeId() { return sourceNodeId; }
public void setSourceNodeId(String sourceNodeId) {
this.sourceNodeId = requireText(sourceNodeId, "sourceNodeId");
}
public String getTargetNodeId() { return targetNodeId; }
public void setTargetNodeId(String targetNodeId) {
this.targetNodeId = requireText(targetNodeId, "targetNodeId");
}
public RelationType getRelationType() { return relationType; }
public void setRelationType(RelationType relationType) { this.relationType = relationType; }
public HypothesisStatus getStatus() { return status; }
public void setStatus(HypothesisStatus status) { this.status = status; }
public Double getConfidence() { return confidence; }
public void setConfidence(Double confidence) { this.confidence = confidence; }
public String getAnalysisSessionId() { return analysisSessionId; }
public void setAnalysisSessionId(String analysisSessionId) {
this.analysisSessionId = normalizeOptional(analysisSessionId);
this.analysisSessionScopeKey = sessionScopeKeyFor(this.analysisSessionId);
}
public String getAnalysisSessionScopeKey() { return analysisSessionScopeKey; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getRequirementId() { return requirementId; }
public void setRequirementId(Long requirementId) { this.requirementId = requirementId; }
public String getAnalysisSnapshotId() { return analysisSnapshotId; }
public void setAnalysisSnapshotId(String analysisSnapshotId) {
this.analysisSnapshotId = normalizeOptional(analysisSnapshotId);
}
public boolean isAppliedInCurrentAnalysis() { return appliedInCurrentAnalysis; }
public void setAppliedInCurrentAnalysis(boolean appliedInCurrentAnalysis) {
this.appliedInCurrentAnalysis = appliedInCurrentAnalysis;
}
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public String getWorkspaceId() { return workspaceId; }
public void setWorkspaceId(String workspaceId) {
this.workspaceId = normalizeOptional(workspaceId);
this.workspaceScopeKey = scopeKeyFor(this.workspaceId);
}
public String getWorkspaceScopeKey() { return workspaceScopeKey; }
public String getOwnerUsername() { return ownerUsername; }
public void setOwnerUsername(String ownerUsername) {
this.ownerUsername = normalizeOptional(ownerUsername);
}
private static String normalizeOptional(String value) {
return value == null || value.isBlank() ? null : value.strip();
}
private static String requireText(String value, String field) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.strip();
}
}