WorkflowSessionHttpAdapter.java
package org.hammer.audio.workflow.editor.http;
import jakarta.validation.Valid;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import org.hammer.audio.workflow.Workflow;
import org.hammer.audio.workflow.WorkflowOperation;
import org.hammer.audio.workflow.collaboration.OperationActor;
import org.hammer.audio.workflow.collaboration.WorkflowSessionRegistry;
import org.hammer.audio.workflow.editor.WorkflowProjection;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.HistoryCapabilitiesRequest;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.HistoryCapabilitiesResponse;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.HistoryPageResponse;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.HistoryQueryRequest;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.RedoPreviewRequest;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.RedoPreviewResponse;
import org.hammer.audio.workflow.editor.http.WorkflowHistoryApiModels.UndoPreviewResponse;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.ActorIdRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.CreateSessionRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.HistoryCommandResponse;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.JoinSessionRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.PresenceRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.PresenceResponse;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.RedoCommandRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.SessionOperationRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.SessionResponse;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.UndoCommandRequest;
import org.hammer.audio.workflow.editor.http.WorkflowSessionApiModels.UndoPreviewRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
/** Spring MVC REST controller for collaboration-session lifecycle and semantic commands. */
@RestController
@RequestMapping("/workflow/sessions")
public final class WorkflowSessionHttpAdapter {
private static final String SESSION_ID = "sessionId";
private final WorkflowSessionRegistry registry;
/** Creates the lifecycle REST controller. */
public WorkflowSessionHttpAdapter(WorkflowSessionRegistry registry) {
this.registry = Objects.requireNonNull(registry, "registry");
}
/** Creates a session and joins its owner. */
@PostMapping
public ResponseEntity<SessionResponse> create(@Valid @RequestBody CreateSessionRequest request) {
SessionResponse response =
SessionResponse.from(
registry.create(
request.sessionId(),
request.mode(),
request.actor().toDomain(),
request.initialWorkflow()));
URI location =
UriComponentsBuilder.fromPath("/workflow/sessions/{" + SESSION_ID + "}")
.buildAndExpand(response.sessionId())
.encode()
.toUri();
return ResponseEntity.created(location).body(response);
}
/** Lists all current sessions in stable identifier order. */
@GetMapping
public List<SessionResponse> sessions() {
return registry.sessions().stream().map(SessionResponse::from).toList();
}
/** Joins an existing session. */
@PostMapping("/{sessionId}/join")
public SessionResponse join(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody JoinSessionRequest request) {
return SessionResponse.from(registry.join(sessionId, request.toDomain()));
}
/** Leaves a session while retaining it for reconnect. */
@PostMapping("/{sessionId}/leave")
public SessionResponse leave(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody ActorIdRequest request) {
return SessionResponse.from(registry.leave(sessionId, request.actorId()));
}
/** Applies one joined actor's semantic operation and returns the canonical projection. */
@PostMapping("/{sessionId}/operations")
public WorkflowProjection applyOperation(
@PathVariable(SESSION_ID) String sessionId,
@Valid @RequestBody SessionOperationRequest request) {
OperationActor actor = request.actor().toDomain();
WorkflowOperation operation =
WorkflowOperationHttpParser.parse(request.operation(), actor.actorId());
Workflow workflow =
request.expectedRevision() == null
? registry.applyOperation(sessionId, request.mode(), actor, operation)
: registry.applyOperation(
sessionId, request.mode(), actor, request.expectedRevision(), operation);
return WorkflowProjection.fromWorkflow(workflow);
}
/** Returns one bounded newest-first page of durable semantic history. */
@PostMapping("/{sessionId}/history/query")
public HistoryPageResponse history(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody HistoryQueryRequest request) {
return HistoryPageResponse.from(
registry.history(
sessionId,
request.actor().toDomain(),
request.beforeRevision(),
request.resolvedLimit()));
}
/** Returns actor-scoped undo, redo and shared-undo capabilities. */
@PostMapping("/{sessionId}/history/capabilities")
public HistoryCapabilitiesResponse capabilities(
@PathVariable(SESSION_ID) String sessionId,
@Valid @RequestBody HistoryCapabilitiesRequest request) {
return HistoryCapabilitiesResponse.from(
registry.capabilities(sessionId, request.actor().toDomain()));
}
/** Computes an immutable timestamp-aware undo preview at the current semantic revision. */
@PostMapping("/{sessionId}/undo/preview")
public UndoPreviewResponse previewUndo(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody UndoPreviewRequest request) {
return UndoPreviewResponse.from(
registry.previewUndo(sessionId, request.actor().toDomain(), request.targetOperationId()));
}
/** Applies one idempotent revision-aware semantic undo command. */
@PostMapping("/{sessionId}/undo")
public HistoryCommandResponse undo(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody UndoCommandRequest request) {
return HistoryCommandResponse.from(registry.undo(sessionId, request.toDomain()));
}
/** Computes an immutable timestamp-aware redo preview at the current semantic revision. */
@PostMapping("/{sessionId}/redo/preview")
public RedoPreviewResponse previewRedo(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody RedoPreviewRequest request) {
return RedoPreviewResponse.from(
registry.previewRedo(
sessionId, request.actor().toDomain(), request.targetUndoOperationId()));
}
/** Applies one idempotent revision-aware semantic redo command. */
@PostMapping("/{sessionId}/redo")
public HistoryCommandResponse redo(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody RedoCommandRequest request) {
return HistoryCommandResponse.from(registry.redo(sessionId, request.toDomain()));
}
/** Updates non-semantic presence and broadcasts it separately from workflow history. */
@PutMapping("/{sessionId}/presence")
public PresenceResponse updatePresence(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody PresenceRequest request) {
OperationActor actor = request.actor().toDomain();
return PresenceResponse.from(registry.updatePresence(sessionId, actor, request.toDomain()));
}
/** Returns immutable session metadata. */
@GetMapping("/{sessionId}")
public SessionResponse inspect(@PathVariable(SESSION_ID) String sessionId) {
return SessionResponse.from(registry.inspect(sessionId));
}
/** Returns the current server-authoritative workflow projection. */
@GetMapping("/{sessionId}/projection")
public WorkflowProjection projection(@PathVariable(SESSION_ID) String sessionId) {
return WorkflowProjection.fromWorkflow(registry.workflow(sessionId));
}
/** Explicitly closes a session. Only its owner may close it. */
@DeleteMapping("/{sessionId}")
public ResponseEntity<Void> close(
@PathVariable(SESSION_ID) String sessionId, @Valid @RequestBody ActorIdRequest request) {
registry.close(sessionId, request.actorId());
return ResponseEntity.noContent().build();
}
}