AnalysisWorkingDraft.java
package com.taxonomy.analysis.session;
import com.taxonomy.workspace.model.RepositoryTenantIdentity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
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 jakarta.persistence.Version;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.time.Instant;
/**
* Mutable, optimistic-lock protected working copy for the ad-hoc analysis page.
*
* <p>This entity deliberately differs from immutable portfolio requirement
* versions and analysis snapshots. It preserves unfinished browser work across
* reloads and devices until the user either discards it or promotes it into a
* project requirement.</p>
*/
@Entity
@Table(name = "analysis_working_draft", indexes = {
@Index(name = "idx_analysis_draft_workspace_user",
columnList = "workspace_id,username"),
@Index(name = "idx_analysis_draft_scope", columnList = "scope_key")
}, uniqueConstraints = {
@UniqueConstraint(name = "uq_analysis_draft_scope_user",
columnNames = {"scope_key", "username"})
})
public class AnalysisWorkingDraft {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "scope_key", nullable = false,
length = RepositoryTenantIdentity.MAX_SCOPE_KEY_LENGTH)
private String scopeKey;
@Column(name = "workspace_id", nullable = false, length = 320)
private String workspaceId;
@Column(nullable = false, length = 160)
private String username;
/**
* Materialized long text rather than a JDBC CLOB/OID locator.
* Frequent autosaves must replace one row value without accumulating
* PostgreSQL large objects that require separate lifecycle management.
*/
@JdbcTypeCode(SqlTypes.LONG32VARCHAR)
@Column(name = "payload_json", nullable = false)
private String payloadJson;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "row_version", nullable = false)
private long rowVersion;
protected AnalysisWorkingDraft() {
}
public AnalysisWorkingDraft(String scopeKey,
String workspaceId,
String username,
String payloadJson,
Instant now) {
this.scopeKey = scopeKey;
this.workspaceId = workspaceId;
this.username = username;
this.payloadJson = payloadJson;
this.createdAt = now;
this.updatedAt = now;
validate();
}
public void replacePayload(String payloadJson, Instant now) {
this.payloadJson = payloadJson;
this.updatedAt = now;
validate();
}
@PrePersist
@PreUpdate
private void validate() {
requireText(scopeKey, "scopeKey");
requireText(workspaceId, "workspaceId");
requireText(username, "username");
requireText(payloadJson, "payloadJson");
if (createdAt == null || updatedAt == null) {
throw new IllegalArgumentException("Draft timestamps must not be null");
}
}
private static void requireText(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " must not be blank");
}
}
public Long getId() {
return id;
}
public String getScopeKey() {
return scopeKey;
}
public String getWorkspaceId() {
return workspaceId;
}
public String getUsername() {
return username;
}
public String getPayloadJson() {
return payloadJson;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public long getRowVersion() {
return rowVersion;
}
}