package ai.dynamixs.model.builder; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openbpmn.bpmn.elements.Activity; import org.openbpmn.bpmn.elements.BPMNProcess; import org.openbpmn.bpmn.elements.Event; import org.openbpmn.bpmn.elements.core.BPMNPoint; import org.openbpmn.bpmn.exceptions.BPMNModelException; /** * Parses a process description file and generates BPMN elements. * Uses LayoutCalculator for positioning of elements. * * Supports both plain text and Markdown-formatted descriptions. */ public class ProcessDescriptionParser { private static final Logger logger = Logger.getLogger(ProcessDescriptionParser.class.getName()); // Regex patterns (flexible for plain text and Markdown) private static final Pattern TASK_PATTERN = Pattern.compile( "\\|-\\s*\\[Task:\\s*(\\d+)\\]\\s*(.+)$"); private static final Pattern EVENT_PATTERN = Pattern.compile( "\\|--\\s*\\[Event:\\s*(\\d+)\\]\\s*(.+?)\\s*-->\\s*\\[Task:\\s*(\\d+)\\]"); private static final Pattern TEAM_PATTERN = Pattern.compile( "Team:\\s*(.+)$"); // Layout calculator private LayoutCalculator layoutCalculator = new LayoutCalculator(); // Storage for created elements private Map tasks = new HashMap<>(); private List events = new ArrayList<>(); private Map taskLanes = new HashMap<>(); private int sequenceFlowCounter = 1; /** * Internal class to hold event data before creating BPMN elements. */ private static class EventData { int eventId; String name; int sourceTaskId; int targetTaskId; EventData(int eventId, String name, int sourceTaskId, int targetTaskId) { this.eventId = eventId; this.name = name; this.sourceTaskId = sourceTaskId; this.targetTaskId = targetTaskId; } String getKey() { return sourceTaskId + "_" + eventId; } } /** * Simple data holder for tasks during parsing. */ private static class TaskData { int id; String name; String description; String team; TaskData(int id, String name) { this.id = id; this.name = name; } } /** * Removes Markdown formatting (**, *, etc.) from a string. */ private String cleanMarkdown(String text) { if (text == null) return null; // Remove ** and * markers String cleaned = text.replaceAll("\\*\\*", "").replaceAll("\\*", ""); // Remove trailing comments like (Iteration für Korrekturen) cleaned = cleaned.replaceAll("\\s*\\([^)]*\\)\\s*$", ""); return cleaned.trim(); } /** * Parses the description file and creates all BPMN elements in the process. */ public void parseAndGenerate(String filePath, BPMNProcess process, Activity referenceTask, Event referenceEvent) throws IOException, BPMNModelException { tasks.clear(); events.clear(); taskLanes.clear(); // Parse file and collect task/event data List taskDataList = parseFile(filePath); // Collect unique lanes (in order of first appearance) List laneNames = new ArrayList<>(); for (TaskData taskData : taskDataList) { if (taskData.team != null && !laneNames.contains(taskData.team)) { laneNames.add(taskData.team); } taskLanes.put(taskData.id, taskData.team); } // Create BPMN lanes for (String laneName : laneNames) { process.addLane(laneName); logger.info("Created lane: " + laneName); } // Calculate layout layoutCalculator.calculateLanePositions(laneNames); List taskIds = taskDataList.stream().map(t -> t.id).toList(); layoutCalculator.calculateTaskPositions(taskIds, taskLanes); // Calculate layout for all events for (EventData eventData : events) { int eventIndex = getEventIndex(eventData.sourceTaskId, eventData.eventId); String laneName = taskLanes.get(eventData.sourceTaskId); layoutCalculator.calculateEventPosition(eventData.sourceTaskId, eventData.eventId, eventIndex, laneName); } // Create BPMN tasks createTasks(taskDataList, process, referenceTask); // Create BPMN events Map createdEvents = createEvents(process, referenceEvent); // Create sequence flows createSequenceFlows(process, createdEvents); logger.info("Generated " + tasks.size() + " tasks, " + createdEvents.size() + " events, " + laneNames.size() + " lanes"); } /** * Parses the file and returns task data list. Also populates events list. */ private List parseFile(String filePath) throws IOException { List taskDataList = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String line; TaskData currentTask = null; StringBuilder descBuilder = new StringBuilder(); boolean readingDesc = false; while ((line = reader.readLine()) != null) { String trimmed = line.trim(); // Match task Matcher taskMatcher = TASK_PATTERN.matcher(trimmed); if (taskMatcher.find()) { if (currentTask != null) { currentTask.description = descBuilder.toString().trim(); } currentTask = new TaskData( Integer.parseInt(taskMatcher.group(1)), cleanMarkdown(taskMatcher.group(2))); taskDataList.add(currentTask); descBuilder = new StringBuilder(); readingDesc = false; continue; } // Match team Matcher teamMatcher = TEAM_PATTERN.matcher(trimmed); if (teamMatcher.find() && currentTask != null) { currentTask.team = cleanMarkdown(teamMatcher.group(1)); continue; } // Match event Matcher eventMatcher = EVENT_PATTERN.matcher(trimmed); if (eventMatcher.find() && currentTask != null) { readingDesc = false; events.add(new EventData( Integer.parseInt(eventMatcher.group(1)), cleanMarkdown(eventMatcher.group(2)), currentTask.id, Integer.parseInt(eventMatcher.group(3)))); continue; } // Description handling if (trimmed.contains("Description:")) { readingDesc = true; continue; } if (readingDesc && currentTask != null && !trimmed.startsWith("|--")) { String descLine = trimmed.replaceAll("^\\|\\s*", "").trim(); if (!descLine.isEmpty()) { if (descBuilder.length() > 0) descBuilder.append(" "); descBuilder.append(descLine); } } } if (currentTask != null) { currentTask.description = descBuilder.toString().trim(); } } return taskDataList; } /** * Creates BPMN task elements. */ private void createTasks(List taskDataList, BPMNProcess process, Activity referenceTask) throws BPMNModelException { for (TaskData taskData : taskDataList) { double[] pos = layoutCalculator.getTaskPosition(taskData.id); Activity task = (Activity) process.cloneBPMNElementNode(referenceTask); task.setName(taskData.name); task.setPosition(new BPMNPoint(pos[0], pos[1])); task.setAttribute("imixs:processid", String.valueOf(taskData.id)); if (taskData.description != null && !taskData.description.isEmpty()) { task.setDocumentation(taskData.description); } tasks.put(taskData.id, task); logger.info("Created task: " + taskData.id + " - " + taskData.name); } } /** * Creates BPMN event elements. */ private Map createEvents(BPMNProcess process, Event referenceEvent) throws BPMNModelException { Map createdEvents = new HashMap<>(); for (EventData eventData : events) { double[] pos = layoutCalculator.getEventPosition(eventData.sourceTaskId, eventData.eventId); if (pos == null) continue; Event event = (Event) process.cloneBPMNElementNode(referenceEvent); event.setName(eventData.name); event.setPosition(new BPMNPoint(pos[0], pos[1])); // Calculate activity ID: first event = 10, second = 20, etc. (per task) int eventIndex = getEventIndex(eventData.sourceTaskId, eventData.eventId); int activityId = (eventIndex + 1) * 10; event.setAttribute("imixs:activityid", String.valueOf(activityId)); createdEvents.put(eventData.getKey(), event); logger.info("Created event: " + eventData.getKey() + " - " + eventData.name); } return createdEvents; } /** * Creates sequence flows between tasks and events. */ private void createSequenceFlows(BPMNProcess process, Map createdEvents) throws BPMNModelException { for (EventData eventData : events) { Activity sourceTask = tasks.get(eventData.sourceTaskId); Event event = createdEvents.get(eventData.getKey()); Activity targetTask = tasks.get(eventData.targetTaskId); if (sourceTask == null || event == null || targetTask == null) continue; if (eventData.sourceTaskId == eventData.targetTaskId) { // Self-loop (CatchEvent): only sequence flow from event to task process.addSequenceFlow("sf-" + sequenceFlowCounter++, event.getId(), targetTask.getId()); } else { // Normal flow: task -> event -> target task process.addSequenceFlow("sf-" + sequenceFlowCounter++, sourceTask.getId(), event.getId()); process.addSequenceFlow("sf-" + sequenceFlowCounter++, event.getId(), targetTask.getId()); } } } /** * Returns the index of an event among all events of a source task. */ private int getEventIndex(int taskId, int eventId) { int index = 0; for (EventData e : events) { if (e.sourceTaskId == taskId) { if (e.eventId == eventId) return index; index++; } } return 0; } /** * Returns the layout calculator for configuration. */ public LayoutCalculator getLayoutCalculator() { return layoutCalculator; } }