WorkflowDslParser.java
package org.hammer.audio.workflow.dsl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hammer.audio.workflow.DataType;
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.PortDirection;
import org.hammer.audio.workflow.PortMultiplicity;
import org.hammer.audio.workflow.Workflow;
/**
* Deterministic text parser for {@link Workflow} objects serialized by {@link
* WorkflowDslSerializer}.
*
* <p>Rebuilds the full workflow domain model from the canonical DSL text. Instances are stateless
* and may be safely reused across parse invocations.
*
* <p>Owned by the DSL layer. May be used by the persistence facade and application services, but
* must not depend on UI, JGit or execution internals.
*
* @see WorkflowDslSerializer
*/
public final class WorkflowDslParser {
private static final String KEY_METADATA = "metadata";
private static final String KEY_NODES = "nodes";
private static final String KEY_EDGES = "edges";
/**
* Parses a workflow from the canonical DSL text produced by {@link WorkflowDslSerializer}.
*
* @param text DSL text
* @return reconstructed workflow
* @throws WorkflowDslParseException if the text is malformed
*/
public Workflow parse(String text) {
List<String> lines = splitLines(text);
ParserState state = new ParserState(lines);
return parseWorkflow(state);
}
private Workflow parseWorkflow(ParserState state) {
state.expectKeyword("workflow");
String id = state.readField("id");
String name = state.readField("name");
Metadata meta = Metadata.empty();
if (state.peekKey(KEY_METADATA)) {
meta = parseMetadata(state);
}
List<Node> nodes = new ArrayList<>();
if (state.peekKey(KEY_NODES)) {
nodes = parseNodes(state);
}
List<Edge> edges = new ArrayList<>();
if (state.peekKey(KEY_EDGES)) {
edges = parseEdges(state);
}
return new Workflow(id, name, nodes, edges, meta);
}
private List<Node> parseNodes(ParserState state) {
state.expectSectionHeader(KEY_NODES);
List<Node> nodes = new ArrayList<>();
while (state.peekListItem(2)) {
nodes.add(parseNode(state));
}
return nodes;
}
private Node parseNode(ParserState state) {
String id = state.readListItemField("id", 2);
String type = state.readField("type", 3);
String label = state.readField("label", 3);
Metadata metadata = Metadata.empty();
if (state.peekKey(KEY_METADATA, 3)) {
metadata = parseMetadata(state, 3);
}
List<Port> inputPorts = new ArrayList<>();
if (state.peekKey("inputPorts", 3)) {
inputPorts = parsePorts(state, 3);
}
List<Port> outputPorts = new ArrayList<>();
if (state.peekKey("outputPorts", 3)) {
outputPorts = parsePorts(state, 3);
}
return new Node(id, type, label, inputPorts, outputPorts, metadata);
}
private List<Port> parsePorts(ParserState state, int parentIndentLevel) {
state.expectSectionHeader(parentIndentLevel);
List<Port> ports = new ArrayList<>();
while (state.peekListItem(parentIndentLevel + 1)) {
ports.add(parsePort(state, parentIndentLevel + 1));
}
return ports;
}
private Port parsePort(ParserState state, int level) {
String id = state.readListItemField("id", level);
String name = state.readField("name", level + 1);
PortDirection direction = PortDirection.valueOf(state.readField("direction", level + 1));
DataType dataType = new DataType(state.readField("dataType", level + 1));
boolean required = Boolean.parseBoolean(state.readField("required", level + 1));
PortMultiplicity multiplicity =
PortMultiplicity.valueOf(state.readField("multiplicity", level + 1));
Metadata metadata = Metadata.empty();
if (state.peekKey(KEY_METADATA, level + 1)) {
metadata = parseMetadata(state, level + 1);
}
return new Port(id, name, direction, dataType, required, multiplicity, metadata);
}
private List<Edge> parseEdges(ParserState state) {
state.expectSectionHeader(KEY_EDGES);
List<Edge> edges = new ArrayList<>();
while (state.peekListItem(2)) {
edges.add(parseEdge(state));
}
return edges;
}
private Edge parseEdge(ParserState state) {
String id = state.readListItemField("id", 2);
String sourceNodeId = state.readField("sourceNodeId", 3);
String sourcePortId = state.readField("sourcePortId", 3);
String targetNodeId = state.readField("targetNodeId", 3);
String targetPortId = state.readField("targetPortId", 3);
Metadata metadata = Metadata.empty();
if (state.peekKey(KEY_METADATA, 3)) {
metadata = parseMetadata(state, 3);
}
return new Edge(id, sourceNodeId, sourcePortId, targetNodeId, targetPortId, metadata);
}
private Metadata parseMetadata(ParserState state) {
return parseMetadata(state, 1);
}
private Metadata parseMetadata(ParserState state, int parentIndentLevel) {
state.expectSectionHeader(parentIndentLevel);
Map<String, String> entries = new HashMap<>();
while (state.peekScalarField(parentIndentLevel + 1)) {
String[] kv = state.readScalarKV(parentIndentLevel + 1);
entries.put(kv[0], kv[1]);
}
return new Metadata(entries);
}
private static List<String> splitLines(String text) {
List<String> lines = new ArrayList<>();
for (String line : text.split("\n", -1)) {
if (!line.isBlank()) {
lines.add(line);
}
}
return lines;
}
// ---------------------------------------------------------------------------
// Inner parser state
// ---------------------------------------------------------------------------
private static final class ParserState {
private static final String EXPECTED_QUOTE = "Expected '";
private static final String AT_LINE = "' at line ";
private static final String BUT_GOT = " but got: ";
private final List<String> lines;
private int pos;
ParserState(List<String> lines) {
this.lines = lines;
this.pos = 0;
}
private String currentLine() {
if (pos >= lines.size()) {
return null;
}
return lines.get(pos);
}
private int indentOf(String line) {
int count = 0;
for (char c : line.toCharArray()) {
if (c == ' ') {
count++;
} else {
break;
}
}
return count / 2;
}
void expectKeyword(String keyword) {
String line = currentLine();
if (line == null || !line.trim().equals(keyword)) {
throw new WorkflowDslParseException(
EXPECTED_QUOTE + keyword + AT_LINE + pos + BUT_GOT + line);
}
pos++;
}
void expectSectionHeader(String key) {
String line = currentLine();
if (line == null || !line.trim().equals(key + ":")) {
throw new WorkflowDslParseException(
EXPECTED_QUOTE + key + ":" + AT_LINE + pos + BUT_GOT + line);
}
pos++;
}
void expectSectionHeader(int indentLevel) {
String line = currentLine();
if (line == null) {
throw new WorkflowDslParseException("Expected section header at line " + pos);
}
String trimmed = line.trim();
if (!trimmed.endsWith(":")) {
throw new WorkflowDslParseException(
"Expected section header '...:' at line " + pos + BUT_GOT + line);
}
pos++;
}
String readField(String key) {
return readField(key, 1);
}
String readField(String key, int level) {
String line = currentLine();
if (line == null) {
throw new WorkflowDslParseException(EXPECTED_QUOTE + key + AT_LINE + pos);
}
int indent = indentOf(line);
if (indent != level) {
throw new WorkflowDslParseException(
"Expected indent " + level + " for field " + EXPECTED_QUOTE + key + AT_LINE + pos);
}
String trimmed = line.trim();
String prefix = key + ": ";
if (!trimmed.startsWith(prefix)) {
throw new WorkflowDslParseException(
EXPECTED_QUOTE + prefix + AT_LINE + pos + BUT_GOT + trimmed);
}
pos++;
return unescapeValue(trimmed.substring(prefix.length()));
}
String readListItemField(String key, int level) {
String line = currentLine();
if (line == null) {
throw new WorkflowDslParseException("Expected list item at line " + pos);
}
int indent = indentOf(line);
if (indent != level) {
throw new WorkflowDslParseException(
"Expected indent " + level + " for list item at line " + pos);
}
String trimmed = line.trim();
String prefix = "- " + key + ": ";
if (!trimmed.startsWith(prefix)) {
throw new WorkflowDslParseException(
EXPECTED_QUOTE + prefix + AT_LINE + pos + BUT_GOT + trimmed);
}
pos++;
return unescapeValue(trimmed.substring(prefix.length()));
}
boolean peekKey(String key) {
return peekKey(key, 1);
}
boolean peekKey(String key, int level) {
String line = currentLine();
if (line == null) {
return false;
}
int indent = indentOf(line);
if (indent != level) {
return false;
}
String trimmed = line.trim();
return trimmed.equals(key + ":") || trimmed.startsWith(key + ": ");
}
boolean peekListItem(int level) {
String line = currentLine();
if (line == null) {
return false;
}
int indent = indentOf(line);
return indent == level && line.trim().startsWith("- ");
}
boolean peekScalarField(int level) {
String line = currentLine();
if (line == null) {
return false;
}
int indent = indentOf(line);
if (indent != level) {
return false;
}
String trimmed = line.trim();
return !trimmed.startsWith("- ") && !trimmed.endsWith(":") && trimmed.contains(": ");
}
String[] readScalarKV(int level) {
String line = currentLine();
if (line == null) {
throw new WorkflowDslParseException("Expected scalar key-value at line " + pos);
}
String trimmed = line.trim();
int colonIdx = trimmed.indexOf(": ");
if (colonIdx < 0) {
throw new WorkflowDslParseException("Malformed key-value line at " + pos + ": " + trimmed);
}
pos++;
String key = unescapeValue(trimmed.substring(0, colonIdx));
String value = unescapeValue(trimmed.substring(colonIdx + 2));
return new String[] {key, value};
}
private static String unescapeValue(String raw) {
if ("~".equals(raw)) {
return null;
}
if (raw.startsWith("\"") && raw.endsWith("\"") && raw.length() >= 2) {
return unescapeQuotedValue(raw.substring(1, raw.length() - 1));
}
return raw;
}
private static String unescapeQuotedValue(String raw) {
StringBuilder value = new StringBuilder(raw.length());
int index = 0;
while (index < raw.length()) {
char current = raw.charAt(index);
if (current != '\\' || index + 1 >= raw.length()) {
value.append(current);
index++;
continue;
}
char escaped = raw.charAt(index + 1);
switch (escaped) {
case 'n' -> value.append('\n');
case '"' -> value.append('"');
case '\\' -> value.append('\\');
default -> value.append(raw, index, index + 2);
}
index += 2;
}
return value.toString();
}
}
}