WorkflowHistoryCommandExceptionHandler.java
package org.hammer.audio.workflow.editor.http;
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.Map;
import org.hammer.audio.workflow.history.WorkflowHistoryAccessException;
import org.hammer.audio.workflow.history.WorkflowMergeRejectedException;
import org.hammer.audio.workflow.merge.WorkflowMergeModels.Conflict;
import org.hammer.audio.workflow.store.CommitId;
import org.hammer.audio.workflow.store.StaleWorkflowHeadException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** Machine-readable conflict mapping for explicit workflow-history commands. */
@RestControllerAdvice
public final class WorkflowHistoryCommandExceptionHandler {
private static final String PROBLEM_BASE = "https://audio-analyzer.dev/problems/";
/** Maps optimistic-concurrency history mutation conflicts to HTTP 409. */
@ExceptionHandler(StaleWorkflowHeadException.class)
public ProblemDetail handleStaleHead(
StaleWorkflowHeadException exception, HttpServletRequest request) {
ProblemDetail problem =
problem("stale-workflow-head", "Stale workflow head", exception.getMessage(), request);
problem.setProperty("code", "STALE_WORKFLOW_HEAD");
problem.setProperty("branch", exception.branch());
problem.setProperty("expectedHeadCommitId", value(exception.expectedHead()));
problem.setProperty("actualHeadCommitId", value(exception.actualHead()));
return problem;
}
/** Maps unresolved semantic conflicts or invalid merged graphs to HTTP 409. */
@ExceptionHandler(WorkflowMergeRejectedException.class)
public ProblemDetail handleMergeRejected(
WorkflowMergeRejectedException exception, HttpServletRequest request) {
ProblemDetail problem =
problem(
"workflow-merge-rejected", "Workflow merge rejected", exception.getMessage(), request);
problem.setProperty("code", "WORKFLOW_MERGE_REJECTED");
problem.setProperty(
"conflicts", exception.unresolvedConflicts().stream().map(this::conflict).toList());
problem.setProperty("validationViolations", exception.validationViolations());
return problem;
}
/** Maps active collaboration history mutation conflicts to HTTP 409. */
@ExceptionHandler(WorkflowHistoryAccessException.class)
public ProblemDetail handleAccessConflict(
WorkflowHistoryAccessException exception, HttpServletRequest request) {
ProblemDetail problem =
problem(
"workflow-history-access-conflict",
"Workflow history access conflict",
exception.getMessage(),
request);
problem.setProperty("code", "WORKFLOW_HISTORY_ACCESS_CONFLICT");
problem.setProperty("branch", exception.branch());
problem.setProperty("workflowId", exception.workflowId());
return problem;
}
private Map<String, Object> conflict(Conflict conflict) {
return Map.of(
"conflictId", conflict.conflictId(),
"kind", conflict.kind().name(),
"elementKind", conflict.elementKind().name(),
"elementId", conflict.elementId(),
"fieldPath", conflict.fieldPath(),
"allowedChoices", conflict.allowedChoices());
}
private static ProblemDetail problem(
String name, String title, String detail, HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, detail);
problem.setTitle(title);
problem.setType(URI.create(PROBLEM_BASE + name));
problem.setInstance(URI.create(request.getRequestURI()));
return problem;
}
private static String value(CommitId commitId) {
return commitId == null ? "" : commitId.value();
}
}