001/* 002 * Copyright 2025 devteam@scivicslab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac.report; 019 020import com.scivicslab.pojoactor.core.ActionResult; 021import com.scivicslab.pojoactor.core.CallableByActionName; 022import com.scivicslab.pojoactor.core.JsonState; 023import com.scivicslab.pojoactor.workflow.ActorSystemAware; 024import com.scivicslab.pojoactor.workflow.IIActorRef; 025import com.scivicslab.pojoactor.workflow.IIActorSystem; 026import com.scivicslab.pojoactor.workflow.IIActorRefAware; 027 028import org.json.JSONObject; 029import org.yaml.snakeyaml.Yaml; 030 031import java.io.InputStream; 032import java.nio.file.Files; 033import java.nio.file.Path; 034import java.nio.file.Paths; 035import java.util.ArrayList; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039import java.util.logging.Logger; 040 041/** 042 * Section-based workflow report builder. 043 * 044 * <p>Assembles report sections from two sources:</p> 045 * <ol> 046 * <li><strong>Legacy sections</strong> - Added via {@code addWorkflowInfo} and {@code addJsonStateSection}</li> 047 * <li><strong>Child actor sections</strong> - {@link SectionBuilder} actors created as children</li> 048 * </ol> 049 * 050 * <h2>Usage with child actor sections (recommended):</h2> 051 * <pre>{@code 052 * steps: 053 * - states: ["0", "1"] 054 * actions: 055 * - actor: loader 056 * method: createChild 057 * arguments: ["ROOT", "reportBuilder", "...ReportBuilder"] 058 * - actor: loader 059 * method: createChild 060 * arguments: ["reportBuilder", "wfName", "...WorkflowNameSection"] 061 * - actor: loader 062 * method: createChild 063 * arguments: ["reportBuilder", "wfDesc", "...WorkflowDescriptionSection"] 064 * 065 * - states: ["1", "end"] 066 * actions: 067 * - actor: reportBuilder 068 * method: report 069 * }</pre> 070 * 071 * <h2>Legacy usage:</h2> 072 * <pre>{@code 073 * steps: 074 * - states: ["0", "1"] 075 * actions: 076 * - actor: loader 077 * method: createChild 078 * arguments: ["ROOT", "reportBuilder", "...ReportBuilder"] 079 * - actor: reportBuilder 080 * method: addWorkflowInfo 081 * 082 * - states: ["1", "end"] 083 * actions: 084 * - actor: reportBuilder 085 * method: report 086 * }</pre> 087 * 088 * <h2>Actions:</h2> 089 * <ul> 090 * <li>{@code addWorkflowInfo} - Add workflow metadata section (legacy)</li> 091 * <li>{@code addJsonStateSection} - Add actor's JsonState as YAML (legacy)</li> 092 * <li>{@code report} - Build and output the report to outputMultiplexer</li> 093 * </ul> 094 * 095 * @author devteam@scivicslab.com 096 * @since 2.15.0 097 */ 098public class ReportBuilder implements CallableByActionName, ActorSystemAware, IIActorRefAware { 099 100 private static final String CLASS_NAME = ReportBuilder.class.getName(); 101 private static final Logger logger = Logger.getLogger(CLASS_NAME); 102 103 private final List<ReportSection> sections = new ArrayList<>(); 104 private IIActorSystem system; 105 private IIActorRef<?> selfRef; 106 107 /** 108 * Default constructor for use with loader.createChild. 109 */ 110 public ReportBuilder() { 111 } 112 113 @Override 114 public void setActorSystem(IIActorSystem system) { 115 this.system = system; 116 logger.info("ReportBuilder: ActorSystem set"); 117 } 118 119 @Override 120 public void setIIActorRef(IIActorRef<?> actorRef) { 121 this.selfRef = actorRef; 122 logger.info("ReportBuilder: IIActorRef set"); 123 } 124 125 // ======================================================================== 126 // CallableByActionName implementation 127 // ======================================================================== 128 129 @Override 130 public ActionResult callByActionName(String actionName, String args) { 131 logger.info("ReportBuilder.callByActionName: " + actionName); 132 133 return switch (actionName) { 134 case "addWorkflowInfo" -> addWorkflowInfo(args); 135 case "addJsonStateSection" -> addJsonStateSection(args); 136 case "report" -> report(args); 137 default -> new ActionResult(false, "Unknown action: " + actionName); 138 }; 139 } 140 141 // ======================================================================== 142 // Actions 143 // ======================================================================== 144 145 /** 146 * Adds workflow info section. 147 */ 148 private ActionResult addWorkflowInfo(String args) { 149 String workflowPath = getWorkflowPathFromNodeGroup(); 150 if (workflowPath == null) { 151 return new ActionResult(false, "Could not get workflow path from nodeGroup"); 152 } 153 154 String name = null; 155 String description = null; 156 157 // Try to read workflow YAML for name and description 158 try { 159 Path path = Paths.get(workflowPath); 160 if (!Files.exists(path)) { 161 path = Paths.get(System.getProperty("user.dir"), workflowPath); 162 } 163 164 if (Files.exists(path)) { 165 try (InputStream is = Files.newInputStream(path)) { 166 Yaml yaml = new Yaml(); 167 Map<String, Object> data = yaml.load(is); 168 if (data != null) { 169 name = (String) data.get("name"); 170 Object descObj = data.get("description"); 171 description = descObj != null ? descObj.toString().trim() : null; 172 } 173 } 174 } 175 } catch (Exception e) { 176 logger.warning("ReportBuilder.addWorkflowInfo: Could not read workflow file: " + e.getMessage()); 177 } 178 179 addSection(new WorkflowInfoSection(workflowPath, name, description)); 180 return new ActionResult(true, "Workflow info section added"); 181 } 182 183 /** 184 * Adds JsonState section for specified actor. 185 */ 186 private ActionResult addJsonStateSection(String args) { 187 String actorName; 188 String path = ""; 189 190 try { 191 JSONObject json = new JSONObject(args); 192 actorName = json.getString("actor"); 193 path = json.optString("path", ""); 194 } catch (Exception e) { 195 return new ActionResult(false, "Invalid arguments: " + e.getMessage() + 196 ". Expected: {\"actor\": \"name\", \"path\": \"optional\"}"); 197 } 198 199 if (system == null) { 200 return new ActionResult(false, "ActorSystem not available"); 201 } 202 203 IIActorRef<?> targetActor = system.getIIActor(actorName); 204 if (targetActor == null) { 205 return new ActionResult(false, "Actor not found: " + actorName); 206 } 207 208 JsonState jsonState = targetActor.json(); 209 if (jsonState == null) { 210 return new ActionResult(false, "Actor has no JsonState: " + actorName); 211 } 212 213 String yamlContent = jsonState.toStringOfYaml(path); 214 addSection(new JsonStateSection(actorName, yamlContent)); 215 216 return new ActionResult(true, "JsonState section added for " + actorName); 217 } 218 219 /** 220 * Builds and outputs the report. 221 */ 222 private ActionResult report(String args) { 223 String reportContent = build(); 224 reportToMultiplexer(reportContent); 225 return new ActionResult(true, reportContent); 226 } 227 228 // ======================================================================== 229 // Core Methods 230 // ======================================================================== 231 232 /** 233 * Adds a section to the report. 234 * 235 * @param section the section to add 236 */ 237 public void addSection(ReportSection section) { 238 if (section != null) { 239 sections.add(section); 240 } 241 } 242 243 /** 244 * Builds the report string. 245 * 246 * <p>Collects sections from two sources in order:</p> 247 * <ol> 248 * <li>Legacy sections added via {@code addWorkflowInfo} and {@code addJsonStateSection}</li> 249 * <li>Child actor sections implementing {@link SectionBuilder} (in creation order)</li> 250 * </ol> 251 * 252 * @return the formatted report string 253 */ 254 public String build() { 255 StringBuilder sb = new StringBuilder(); 256 257 // Header 258 sb.append("=== Workflow Execution Report ===\n"); 259 260 // 1. Output legacy sections first (in order added) 261 for (ReportSection section : sections) { 262 appendSection(sb, section.getTitle(), section.getContent()); 263 } 264 265 // 2. Output child actor sections (in creation order) 266 if (selfRef != null && system != null) { 267 Set<String> childNames = selfRef.getNamesOfChildren(); 268 for (String childName : childNames) { 269 IIActorRef<?> childRef = system.getIIActor(childName); 270 if (childRef == null) continue; 271 272 // Call generate action on the child 273 ActionResult generateResult = childRef.callByActionName("generate", ""); 274 if (!generateResult.isSuccess()) { 275 // Child doesn't support generate action, skip it 276 continue; 277 } 278 279 String content = generateResult.getResult(); 280 if (content == null || content.isEmpty()) { 281 continue; 282 } 283 284 // Get title (may be empty) 285 String title = null; 286 ActionResult titleResult = childRef.callByActionName("getTitle", ""); 287 if (titleResult.isSuccess()) { 288 String t = titleResult.getResult(); 289 if (t != null && !t.isEmpty()) { 290 title = t; 291 } 292 } 293 294 appendSection(sb, title, content); 295 } 296 } 297 298 return sb.toString(); 299 } 300 301 private void appendSection(StringBuilder sb, String title, String content) { 302 if (content != null && !content.isEmpty()) { 303 if (title != null && !title.isEmpty()) { 304 sb.append("\n--- ").append(title).append(" ---\n"); 305 } 306 sb.append(content).append("\n"); 307 } 308 } 309 310 // ======================================================================== 311 // Helper Methods 312 // ======================================================================== 313 314 private String getWorkflowPathFromNodeGroup() { 315 if (system == null) return null; 316 317 IIActorRef<?> nodeGroup = system.getIIActor("nodeGroup"); 318 if (nodeGroup == null) return null; 319 320 ActionResult result = nodeGroup.callByActionName("getWorkflowPath", ""); 321 return result.isSuccess() ? result.getResult() : null; 322 } 323 324 private void reportToMultiplexer(String data) { 325 if (system == null) return; 326 327 IIActorRef<?> multiplexer = system.getIIActor("outputMultiplexer"); 328 if (multiplexer == null) return; 329 330 JSONObject arg = new JSONObject(); 331 arg.put("source", "report-builder"); 332 arg.put("type", "plugin-result"); 333 arg.put("data", data); 334 multiplexer.callByActionName("add", arg.toString()); 335 } 336}