GlspWorkflowAdapter.java
package org.hammer.audio.workflow.editor.glsp;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.workflow.Edge;
import org.hammer.audio.workflow.WorkflowOperation;
import org.hammer.audio.workflow.editor.WorkflowEditorService;
import org.hammer.audio.workflow.editor.WorkflowProjection;
/**
* Executable protocol-level GLSP spike adapter for issue #219.
*
* <p>The adapter deliberately does not own workflow state. It derives a small GModel-shaped
* projection from {@link WorkflowProjection}, translates GLSP-style actions into semantic {@link
* WorkflowOperation}s and delegates validation and mutation to {@link WorkflowEditorService}.
*/
public final class GlspWorkflowAdapter {
private final WorkflowEditorService editorService;
public GlspWorkflowAdapter(WorkflowEditorService editorService) {
this.editorService = Objects.requireNonNull(editorService, "editorService");
}
/** Returns the current server-authoritative graph as a GLSP-shaped immutable projection. */
public GGraph currentGraph() {
return toGGraph(editorService.currentProjection());
}
/** Applies one GLSP-style action and returns the rebuilt graph projection. */
public GGraph apply(Action action) {
Objects.requireNonNull(action, "action");
WorkflowProjection current = editorService.currentProjection();
WorkflowOperation operation = toWorkflowOperation(action, current);
return toGGraph(editorService.applyOperation(operation));
}
private static WorkflowOperation toWorkflowOperation(
Action action, WorkflowProjection currentProjection) {
return switch (action) {
case CreateEdgeAction create ->
new WorkflowOperation.ConnectPorts(
create.operationId(),
create.timestamp(),
create.author(),
new Edge(
create.edgeId(),
create.sourceNodeId(),
create.sourcePortId(),
create.targetNodeId(),
create.targetPortId()));
case DeleteEdgeAction delete -> {
WorkflowProjection.EdgeProjection edge =
currentProjection.edges().stream()
.filter(candidate -> candidate.id().equals(delete.edgeId()))
.findFirst()
.orElseThrow(
() -> new IllegalArgumentException("GLSP edge not found: " + delete.edgeId()));
Edge snapshot =
new Edge(
edge.id(), edge.source(), edge.sourceHandle(), edge.target(), edge.targetHandle());
yield new WorkflowOperation.DisconnectPorts(
delete.operationId(), delete.timestamp(), delete.author(), delete.edgeId(), snapshot);
}
case ChangePropertyAction change -> {
WorkflowProjection.NodeProjection node =
currentProjection.nodes().stream()
.filter(candidate -> candidate.id().equals(change.nodeId()))
.findFirst()
.orElseThrow(
() -> new IllegalArgumentException("GLSP node not found: " + change.nodeId()));
yield new WorkflowOperation.UpdateProperty(
change.operationId(),
change.timestamp(),
change.author(),
WorkflowOperation.PropertyTarget.NODE,
change.nodeId(),
change.propertyKey(),
node.properties().get(change.propertyKey()),
change.newValue());
}
};
}
private static GGraph toGGraph(WorkflowProjection projection) {
List<GNode> nodes =
projection.nodes().stream()
.map(
node ->
new GNode(
node.id(),
node.label(),
node.type(),
node.inputHandles().stream()
.map(
port ->
new GPort(
node.id() + "::" + port.id(),
port.id(),
port.name(),
port.dataType(),
PortKind.INPUT))
.toList(),
node.outputHandles().stream()
.map(
port ->
new GPort(
node.id() + "::" + port.id(),
port.id(),
port.name(),
port.dataType(),
PortKind.OUTPUT))
.toList(),
node.properties()))
.toList();
List<GEdge> edges =
projection.edges().stream()
.map(
edge ->
new GEdge(
edge.id(),
edge.source() + "::" + edge.sourceHandle(),
edge.target() + "::" + edge.targetHandle()))
.toList();
return new GGraph(projection.workflowId(), projection.workflowName(), nodes, edges);
}
/** Common metadata carried by every GLSP action translated by this adapter. */
public sealed interface Action permits CreateEdgeAction, DeleteEdgeAction, ChangePropertyAction {
String operationId();
Instant timestamp();
String author();
}
/**
* GLSP-style request to create a typed connection between two semantic workflow ports.
*
* @param operationId stable semantic operation identifier
* @param timestamp operation timestamp
* @param author operation author
* @param edgeId stable edge identifier
* @param sourceNodeId source node identifier
* @param sourcePortId source output-port identifier
* @param targetNodeId target node identifier
* @param targetPortId target input-port identifier
*/
public record CreateEdgeAction(
String operationId,
Instant timestamp,
String author,
String edgeId,
String sourceNodeId,
String sourcePortId,
String targetNodeId,
String targetPortId)
implements Action {
public CreateEdgeAction {
Objects.requireNonNull(operationId, "operationId");
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(author, "author");
Objects.requireNonNull(edgeId, "edgeId");
Objects.requireNonNull(sourceNodeId, "sourceNodeId");
Objects.requireNonNull(sourcePortId, "sourcePortId");
Objects.requireNonNull(targetNodeId, "targetNodeId");
Objects.requireNonNull(targetPortId, "targetPortId");
}
}
/**
* GLSP-style request to remove an existing semantic workflow edge.
*
* @param operationId stable semantic operation identifier
* @param timestamp operation timestamp
* @param author operation author
* @param edgeId identifier of the edge to remove
*/
public record DeleteEdgeAction(
String operationId, Instant timestamp, String author, String edgeId) implements Action {
public DeleteEdgeAction {
Objects.requireNonNull(operationId, "operationId");
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(author, "author");
Objects.requireNonNull(edgeId, "edgeId");
}
}
/**
* GLSP-style request to update one semantic node property.
*
* @param operationId stable semantic operation identifier
* @param timestamp operation timestamp
* @param author operation author
* @param nodeId target node identifier
* @param propertyKey semantic property key
* @param newValue replacement property value
*/
public record ChangePropertyAction(
String operationId,
Instant timestamp,
String author,
String nodeId,
String propertyKey,
String newValue)
implements Action {
public ChangePropertyAction {
Objects.requireNonNull(operationId, "operationId");
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(author, "author");
Objects.requireNonNull(nodeId, "nodeId");
Objects.requireNonNull(propertyKey, "propertyKey");
Objects.requireNonNull(newValue, "newValue");
}
}
/**
* Immutable GLSP-shaped graph projection derived from the canonical workflow model.
*
* @param id workflow identifier
* @param label workflow label
* @param nodes projected nodes
* @param edges projected edges
*/
public record GGraph(String id, String label, List<GNode> nodes, List<GEdge> edges) {
public GGraph {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(label, "label");
Objects.requireNonNull(nodes, "nodes");
Objects.requireNonNull(edges, "edges");
nodes = List.copyOf(nodes);
edges = List.copyOf(edges);
}
}
/**
* Immutable GLSP-shaped node projection with typed input and output ports.
*
* @param id node identifier
* @param label node label
* @param type semantic node type
* @param inputPorts projected input ports
* @param outputPorts projected output ports
* @param properties projected semantic properties
*/
public record GNode(
String id,
String label,
String type,
List<GPort> inputPorts,
List<GPort> outputPorts,
Map<String, String> properties) {
public GNode {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(label, "label");
Objects.requireNonNull(type, "type");
Objects.requireNonNull(inputPorts, "inputPorts");
Objects.requireNonNull(outputPorts, "outputPorts");
Objects.requireNonNull(properties, "properties");
inputPorts = List.copyOf(inputPorts);
outputPorts = List.copyOf(outputPorts);
properties = Map.copyOf(properties);
}
}
/**
* Immutable typed-port projection used as a GLSP connection endpoint.
*
* @param id adapter-level globally unique port identifier
* @param semanticPortId semantic workflow port identifier
* @param label human-readable port label
* @param dataType semantic workflow data-type identifier
* @param kind input or output direction
*/
public record GPort(
String id, String semanticPortId, String label, String dataType, PortKind kind) {
public GPort {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(semanticPortId, "semanticPortId");
Objects.requireNonNull(label, "label");
Objects.requireNonNull(dataType, "dataType");
Objects.requireNonNull(kind, "kind");
}
}
/**
* Immutable GLSP-shaped edge projection between two projected port identifiers.
*
* @param id edge identifier
* @param sourcePortId projected source-port identifier
* @param targetPortId projected target-port identifier
*/
public record GEdge(String id, String sourcePortId, String targetPortId) {
public GEdge {
Objects.requireNonNull(id, "id");
Objects.requireNonNull(sourcePortId, "sourcePortId");
Objects.requireNonNull(targetPortId, "targetPortId");
}
}
/** Direction of a projected GLSP port. */
public enum PortKind {
INPUT,
OUTPUT
}
}