WorkflowOperationPersistenceCodec.java
package org.hammer.audio.workflow.collaboration.store;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.workflow.WorkflowOperation;
/** Converts semantic workflow operations into deterministic durable persistence data. */
public final class WorkflowOperationPersistenceCodec {
private static final int MAX_AFFECTED_OBJECTS = 1_000_000;
private WorkflowOperationPersistenceCodec() {
// Utility class.
}
/** Encodes one ordinary forward operation. */
public static WorkflowOperationPersistenceData encode(WorkflowOperation operation) {
WorkflowOperation requiredOperation = Objects.requireNonNull(operation, "operation");
return encode(
requiredOperation,
WorkflowOperationCommandMetadata.normal(requiredOperation.operationId()));
}
/**
* Encodes one operation for durable append, restart-safe idempotency and semantic reconstruction.
*
* @param operation semantic workflow operation
* @param command durable normal/undo/redo command relation
* @return deterministic persistence data
*/
public static WorkflowOperationPersistenceData encode(
WorkflowOperation operation, WorkflowOperationCommandMetadata command) {
WorkflowOperation requiredOperation = Objects.requireNonNull(operation, "operation");
WorkflowOperationBodyCodec.EncodedBody body =
WorkflowOperationBodyCodec.encode(requiredOperation);
return new WorkflowOperationPersistenceData(
requiredOperation.operationId(),
requiredOperation.author(),
requiredOperation.getClass().getSimpleName(),
requiredOperation.timestamp(),
encodeSemanticIdentity(requiredOperation),
body.version(),
body.body(),
Objects.requireNonNull(command, "command"));
}
/**
* Compares a durable operation with a retry candidate without applying it again.
*
* <p>The timestamp is intentionally excluded from the legacy fingerprint to preserve the existing
* command-idempotency contract. New rows additionally compare the complete operation body after
* normalizing the candidate to the stored timestamp, because a transport retry may recreate the
* same semantic operation with a new timestamp.
*
* @param stored durable accepted operation
* @param candidate semantic retry candidate
* @return whether both describe the same accepted semantic command
*/
public static boolean matches(StoredWorkflowOperation stored, WorkflowOperation candidate) {
StoredWorkflowOperation requiredStored = Objects.requireNonNull(stored, "stored");
WorkflowOperationPersistenceData encodedCandidate = encode(candidate, requiredStored.command());
boolean identityMatches =
requiredStored.operationId().equals(encodedCandidate.operationId())
&& requiredStored.actorId().equals(encodedCandidate.actorId())
&& requiredStored.operationType().equals(encodedCandidate.operationType())
&& requiredStored.payload().equals(encodedCandidate.payload());
if (!identityMatches || !requiredStored.hasOperationBody()) {
return identityMatches;
}
WorkflowOperation normalizedCandidate =
WorkflowOperationBodyCodec.reidentify(
candidate, candidate.operationId(), requiredStored.occurredAt(), candidate.author());
WorkflowOperationBodyCodec.EncodedBody normalizedBody =
WorkflowOperationBodyCodec.encode(normalizedCandidate);
return requiredStored.bodyVersion() == normalizedBody.version()
&& requiredStored.operationBody().equals(normalizedBody.body());
}
/**
* Encodes a semantic payload map in stable key order using unambiguous length prefixes.
*
* @param payload semantic operation payload
* @return deterministic encoded payload
*/
public static String encodePayload(Map<String, String> payload) {
Map<String, String> requiredPayload = Objects.requireNonNull(payload, "payload");
StringBuilder encoded = new StringBuilder();
requiredPayload.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(
entry -> {
appendValue(encoded, entry.getKey());
appendValue(encoded, entry.getValue());
});
return encoded.toString();
}
/**
* Decodes the affected-object prefix retained by legacy semantic identity payloads.
*
* <p>The remaining operation-specific payload map is deliberately ignored. This method exists so
* read-only history can still describe legacy rows without claiming that their complete semantic
* bodies are reconstructible.
*
* @param payload deterministic semantic identity payload
* @return immutable affected object identifiers in their original order
*/
public static List<String> decodeAffectedObjectIds(String payload) {
String requiredPayload = Objects.requireNonNull(payload, "payload");
Cursor cursor = new Cursor(requiredPayload);
int size = cursor.readNonNegativeLength("affected object count");
if (size > MAX_AFFECTED_OBJECTS) {
throw new IllegalArgumentException("affected object count is too large: " + size);
}
List<String> affectedObjectIds = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
affectedObjectIds.add(cursor.readValue("affected object id"));
}
return List.copyOf(affectedObjectIds);
}
private static String encodeSemanticIdentity(WorkflowOperation operation) {
StringBuilder encoded = new StringBuilder();
appendList(encoded, operation.affectedObjectIds());
encoded.append(encodePayload(operation.payload()));
return encoded.toString();
}
private static void appendList(StringBuilder encoded, List<String> values) {
List<String> requiredValues = List.copyOf(Objects.requireNonNull(values, "values"));
encoded.append(requiredValues.size()).append(':');
for (String value : requiredValues) {
appendValue(encoded, value);
}
}
private static void appendValue(StringBuilder encoded, String value) {
if (value == null) {
encoded.append("-1:");
return;
}
encoded.append(value.length()).append(':').append(value);
}
private static final class Cursor {
private final String value;
private int offset;
Cursor(String value) {
this.value = value;
}
int readNonNegativeLength(String description) {
int delimiter = value.indexOf(':', offset);
if (delimiter < 0) {
throw malformed(description);
}
int parsed;
try {
parsed = Integer.parseInt(value.substring(offset, delimiter));
} catch (NumberFormatException exception) {
throw new IllegalArgumentException(
"Invalid " + description + " in semantic identity", exception);
}
if (parsed < 0) {
throw malformed(description);
}
offset = delimiter + 1;
return parsed;
}
String readValue(String description) {
int length = readNonNegativeLength(description + " length");
int end;
try {
end = Math.addExact(offset, length);
} catch (ArithmeticException exception) {
throw new IllegalArgumentException(
"Invalid " + description + " in semantic identity", exception);
}
if (end > value.length()) {
throw malformed(description);
}
String parsed = value.substring(offset, end);
offset = end;
return parsed;
}
private IllegalArgumentException malformed(String description) {
return new IllegalArgumentException("Malformed " + description + " in semantic identity");
}
}
}