WorkflowMergeDraft.java

package org.hammer.audio.workflow.merge;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.UnaryOperator;
import org.hammer.audio.workflow.Edge;
import org.hammer.audio.workflow.Metadata;
import org.hammer.audio.workflow.Node;
import org.hammer.audio.workflow.Port;
import org.hammer.audio.workflow.Workflow;

/** Internal deterministic mutable draft used while calculating and resolving one merge. */
final class WorkflowMergeDraft {

  private final String workflowId;
  private final Map<String, Node> nodesById = new ConcurrentHashMap<>();
  private final Map<String, Edge> edgesById = new ConcurrentHashMap<>();
  private final Map<String, String> workflowMetadata = new ConcurrentHashMap<>();
  private String workflowName;

  private WorkflowMergeDraft(Workflow base) {
    workflowId = base.id();
    workflowName = base.name();
    workflowMetadata.putAll(base.metadata().entries());
    for (Node node : base.nodes()) {
      putNode(node);
    }
    for (Edge edge : base.edges()) {
      putEdge(edge);
    }
  }

  static WorkflowMergeDraft from(Workflow base) {
    return new WorkflowMergeDraft(Objects.requireNonNull(base, "base"));
  }

  Workflow workflow() {
    return new Workflow(
        workflowId,
        workflowName,
        nodesById.values().stream().sorted(Comparator.comparing(Node::id)).toList(),
        edgesById.values().stream().sorted(Comparator.comparing(Edge::id)).toList(),
        new Metadata(workflowMetadata));
  }

  void setWorkflowName(String value) {
    workflowName = requireText(value, "workflowName");
  }

  void setWorkflowMetadata(String key, String value) {
    setMetadata(workflowMetadata, key, value);
  }

  void putNode(Node node) {
    Node required = Objects.requireNonNull(node, "node");
    nodesById.put(required.id(), required);
  }

  void removeNode(String nodeId) {
    nodesById.remove(nodeId);
  }

  void setNodeType(String nodeId, String type) {
    updateNode(
        nodeId,
        node ->
            new Node(
                node.id(),
                requireText(type, "type"),
                node.label(),
                node.inputPorts(),
                node.outputPorts(),
                node.metadata()));
  }

  void setNodeLabel(String nodeId, String label) {
    updateNode(
        nodeId,
        node ->
            new Node(
                node.id(),
                node.type(),
                requireText(label, "label"),
                node.inputPorts(),
                node.outputPorts(),
                node.metadata()));
  }

  void setNodeInputPorts(String nodeId, List<Port> ports) {
    updateNode(
        nodeId,
        node ->
            new Node(
                node.id(),
                node.type(),
                node.label(),
                List.copyOf(ports),
                node.outputPorts(),
                node.metadata()));
  }

  void setNodeOutputPorts(String nodeId, List<Port> ports) {
    updateNode(
        nodeId,
        node ->
            new Node(
                node.id(),
                node.type(),
                node.label(),
                node.inputPorts(),
                List.copyOf(ports),
                node.metadata()));
  }

  void setNodeMetadata(String nodeId, String key, String value) {
    updateNode(
        nodeId,
        node -> {
          Map<String, String> entries = new ConcurrentHashMap<>(node.metadata().entries());
          setMetadata(entries, key, value);
          return new Node(
              node.id(),
              node.type(),
              node.label(),
              node.inputPorts(),
              node.outputPorts(),
              new Metadata(entries));
        });
  }

  void putEdge(Edge edge) {
    Edge required = Objects.requireNonNull(edge, "edge");
    edgesById.put(required.id(), required);
  }

  void removeEdge(String edgeId) {
    edgesById.remove(edgeId);
  }

  void setEdgeEndpoints(String edgeId, Edge source) {
    Objects.requireNonNull(source, "source");
    updateEdge(
        edgeId,
        edge ->
            new Edge(
                edge.id(),
                source.sourceNodeId(),
                source.sourcePortId(),
                source.targetNodeId(),
                source.targetPortId(),
                edge.metadata()));
  }

  void setEdgeMetadata(String edgeId, String key, String value) {
    updateEdge(
        edgeId,
        edge -> {
          Map<String, String> entries = new ConcurrentHashMap<>(edge.metadata().entries());
          setMetadata(entries, key, value);
          return new Edge(
              edge.id(),
              edge.sourceNodeId(),
              edge.sourcePortId(),
              edge.targetNodeId(),
              edge.targetPortId(),
              new Metadata(entries));
        });
  }

  void replaceNodeNeighborhood(
      String nodeId, Node node, Collection<Edge> selectedEdges, Set<String> relatedEdgeIds) {
    Objects.requireNonNull(nodeId, "nodeId");
    Objects.requireNonNull(selectedEdges, "selectedEdges");
    Objects.requireNonNull(relatedEdgeIds, "relatedEdgeIds");
    for (String edgeId : relatedEdgeIds) {
      edgesById.remove(edgeId);
    }
    if (node == null) {
      nodesById.remove(nodeId);
      return;
    }
    nodesById.put(nodeId, node);
    for (Edge edge : selectedEdges) {
      if (references(edge, nodeId)) {
        edgesById.put(edge.id(), edge);
      }
    }
  }

  private void updateNode(String nodeId, UnaryOperator<Node> update) {
    Node current = nodesById.get(nodeId);
    if (current == null) {
      throw new IllegalStateException("Merge draft has no node " + nodeId);
    }
    nodesById.put(nodeId, Objects.requireNonNull(update.apply(current), "updated node"));
  }

  private void updateEdge(String edgeId, UnaryOperator<Edge> update) {
    Edge current = edgesById.get(edgeId);
    if (current == null) {
      throw new IllegalStateException("Merge draft has no edge " + edgeId);
    }
    edgesById.put(edgeId, Objects.requireNonNull(update.apply(current), "updated edge"));
  }

  private static void setMetadata(Map<String, String> entries, String key, String value) {
    String requiredKey = requireText(key, "metadata key");
    if (value == null) {
      entries.remove(requiredKey);
    } else {
      entries.put(requiredKey, value);
    }
  }

  static List<Edge> connectedEdges(List<Edge> edges, String nodeId) {
    List<Edge> connected = new ArrayList<>();
    for (Edge edge : edges) {
      if (references(edge, nodeId)) {
        connected.add(edge);
      }
    }
    connected.sort(Comparator.comparing(Edge::id));
    return List.copyOf(connected);
  }

  static boolean references(Edge edge, String nodeId) {
    return edge.sourceNodeId().equals(nodeId) || edge.targetNodeId().equals(nodeId);
  }

  private static String requireText(String value, String name) {
    if (value == null || value.isBlank()) {
      throw new IllegalArgumentException(name + " must not be blank");
    }
    return value;
  }
}