WorkflowOperationEntity.java

package org.hammer.audio.infrastructure.workflow.collaboration.store;

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.Lob;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.time.Instant;
import org.hammer.audio.workflow.WorkflowOperation;
import org.hammer.audio.workflow.collaboration.store.StoredWorkflowOperation;
import org.hammer.audio.workflow.collaboration.store.WorkflowOperationBodyCodec;
import org.hammer.audio.workflow.collaboration.store.WorkflowOperationCommandMetadata;
import org.hammer.audio.workflow.collaboration.store.WorkflowOperationPersistenceData;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

/** Hibernate-owned durable accepted semantic operation. */
@Entity
@Table(
    name = "workflow_collaboration_operation",
    indexes = {
      @Index(name = "idx_workflow_operation_session", columnList = "session_id"),
      @Index(name = "idx_workflow_operation_revision", columnList = "session_id, semantic_revision")
    },
    uniqueConstraints = {
      @UniqueConstraint(name = "uk_workflow_operation_id", columnNames = "operation_id"),
      @UniqueConstraint(
          name = "uk_workflow_operation_sequence",
          columnNames = {"session_id", "operation_sequence"})
    })
public class WorkflowOperationEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "session_id", nullable = false, length = 255)
  private String storedSessionId;

  @Column(name = "operation_id", nullable = false, length = 255)
  private String storedOperationId;

  @Column(name = "actor_id", nullable = false, length = 255)
  private String actorId;

  @Column(name = "operation_type", nullable = false, length = 255)
  private String operationType;

  @Column(name = "occurred_at", nullable = false)
  private Instant occurredAt;

  @Column(name = "operation_sequence", nullable = false)
  private long storedOperationSequence;

  @Column(name = "semantic_revision", nullable = false)
  private long storedSemanticRevision;

  @Lob
  @Column(name = "operation_payload", nullable = false)
  private String payload;

  @Column(name = "operation_body_version")
  private Integer bodyVersion;

  @JdbcTypeCode(SqlTypes.LONG32VARCHAR)
  @Column(name = "operation_body")
  private String operationBody;

  @Column(name = "command_kind", length = 32)
  private String commandKind;

  @Column(name = "command_id", length = 255)
  private String commandId;

  @Column(name = "target_operation_id", length = 255)
  private String targetOperationId;

  protected WorkflowOperationEntity() {
    // Required by Jakarta Persistence.
  }

  static WorkflowOperationEntity accepted(
      String sessionId, WorkflowOperationPersistenceData operation, long sequence, long revision) {
    WorkflowOperationEntity entity = new WorkflowOperationEntity();
    entity.storedSessionId = sessionId;
    entity.storedOperationId = operation.operationId();
    entity.actorId = operation.actorId();
    entity.operationType = operation.operationType();
    entity.occurredAt = operation.occurredAt();
    entity.storedOperationSequence = sequence;
    entity.storedSemanticRevision = revision;
    entity.payload = operation.payload();
    entity.bodyVersion = operation.hasOperationBody() ? operation.bodyVersion() : null;
    entity.operationBody = operation.operationBody();
    entity.commandKind = operation.command().kind().name();
    entity.commandId = operation.command().commandId();
    entity.targetOperationId = operation.command().targetOperationId();
    return entity;
  }

  StoredWorkflowOperation toStoredOperation() {
    WorkflowOperationCommandMetadata command =
        commandKind == null || commandId == null
            ? WorkflowOperationCommandMetadata.normal(storedOperationId)
            : new WorkflowOperationCommandMetadata(
                WorkflowOperationCommandMetadata.Kind.valueOf(commandKind),
                commandId,
                targetOperationId);
    return new StoredWorkflowOperation(
        storedSessionId,
        storedOperationId,
        actorId,
        operationType,
        occurredAt,
        storedOperationSequence,
        storedSemanticRevision,
        payload,
        bodyVersion == null ? 0 : bodyVersion,
        operationBody,
        command);
  }

  boolean hasSameSemanticContent(
      String expectedSessionId, WorkflowOperationPersistenceData candidate) {
    boolean identityMatches =
        storedSessionId.equals(expectedSessionId)
            && actorId.equals(candidate.actorId())
            && operationType.equals(candidate.operationType())
            && payload.equals(candidate.payload())
            && command().equals(candidate.command());
    if (!identityMatches || bodyVersion == null) {
      return identityMatches;
    }
    if (!candidate.hasOperationBody() || bodyVersion != candidate.bodyVersion()) {
      return false;
    }
    WorkflowOperation candidateOperation =
        WorkflowOperationBodyCodec.decode(candidate.bodyVersion(), candidate.operationBody());
    WorkflowOperation normalizedCandidate =
        WorkflowOperationBodyCodec.reidentify(
            candidateOperation, candidate.operationId(), occurredAt, candidate.actorId());
    return operationBody.equals(WorkflowOperationBodyCodec.encode(normalizedCandidate).body());
  }

  private WorkflowOperationCommandMetadata command() {
    return commandKind == null || commandId == null
        ? WorkflowOperationCommandMetadata.normal(storedOperationId)
        : new WorkflowOperationCommandMetadata(
            WorkflowOperationCommandMetadata.Kind.valueOf(commandKind),
            commandId,
            targetOperationId);
  }

  String sessionId() {
    return storedSessionId;
  }

  String operationId() {
    return storedOperationId;
  }

  long operationSequence() {
    return storedOperationSequence;
  }

  long semanticRevision() {
    return storedSemanticRevision;
  }
}