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.accumulator; 019 020import java.io.BufferedWriter; 021import java.io.Closeable; 022import java.io.FileWriter; 023import java.io.IOException; 024import java.io.PrintWriter; 025import java.nio.file.Path; 026import java.util.concurrent.atomic.AtomicInteger; 027 028import com.scivicslab.pojoactor.core.accumulator.Accumulator; 029 030/** 031 * Accumulator that writes output to a text file. 032 * 033 * <p>This accumulator writes all output to a text file as it arrives. 034 * The output format is identical to what appears on the console, 035 * ensuring consistency across all output destinations.</p> 036 * 037 * <h2>Usage</h2> 038 * <pre>{@code 039 * try (FileAccumulator fileAcc = new FileAccumulator(Path.of("run.log"))) { 040 * fileAcc.add("node-1", "stdout", "command output"); 041 * fileAcc.add("workflow", "cowsay", renderedCowsayArt); 042 * } 043 * }</pre> 044 * 045 * @author devteam@scivicslab.com 046 * @since 2.12.0 047 */ 048public class FileAccumulator implements Accumulator, Closeable { 049 050 private final PrintWriter writer; 051 private final Path filePath; 052 private final AtomicInteger count = new AtomicInteger(0); 053 private volatile boolean closed = false; 054 055 /** 056 * Constructs a FileAccumulator that writes to the specified file. 057 * 058 * @param filePath the path to the output file 059 * @throws IOException if the file cannot be opened for writing 060 */ 061 public FileAccumulator(Path filePath) throws IOException { 062 this.filePath = filePath; 063 this.writer = new PrintWriter(new BufferedWriter(new FileWriter(filePath.toFile()))); 064 } 065 066 /** 067 * Constructs a FileAccumulator that writes to the specified file. 068 * 069 * @param filePath the path to the output file as a string 070 * @throws IOException if the file cannot be opened for writing 071 */ 072 public FileAccumulator(String filePath) throws IOException { 073 this(Path.of(filePath)); 074 } 075 076 /** 077 * Returns the path to the output file. 078 * 079 * @return the file path 080 */ 081 public Path getFilePath() { 082 return filePath; 083 } 084 085 @Override 086 public void add(String source, String type, String data) { 087 if (closed || data == null || data.isEmpty()) { 088 count.incrementAndGet(); 089 return; 090 } 091 092 // Format output with fixed-width source prefix on each line 093 String output = formatOutput(source, data); 094 095 synchronized (writer) { 096 writer.print(output); 097 writer.flush(); 098 } 099 count.incrementAndGet(); 100 } 101 102 /** 103 * Formats the output with a fixed-width source prefix on each line. 104 * 105 * <p>Every line of output is prefixed with {@code [source]} where source 106 * is left-justified in a fixed-width field. This allows multi-line output 107 * (such as cowsay ASCII art) to remain properly aligned while still being 108 * identifiable by source.</p> 109 * 110 * @param source the source identifier (e.g., "node-web-01", "cli") 111 * @param data the output data (may contain multiple lines) 112 * @return the formatted output string with prefix on each line 113 */ 114 private String formatOutput(String source, String data) { 115 String prefix = formatPrefix(source); 116 StringBuilder sb = new StringBuilder(); 117 118 String[] lines = data.split("\n", -1); 119 for (int i = 0; i < lines.length; i++) { 120 sb.append(prefix).append(lines[i]); 121 if (i < lines.length - 1) { 122 sb.append("\n"); 123 } 124 } 125 sb.append("\n"); 126 127 return sb.toString(); 128 } 129 130 /** 131 * Creates a prefix from the source name. 132 * 133 * @param source the source identifier 134 * @return formatted prefix like "[node-web-01] " 135 */ 136 private String formatPrefix(String source) { 137 String src = (source != null) ? source : ""; 138 return "[" + src + "] "; 139 } 140 141 @Override 142 public String getSummary() { 143 return "FileAccumulator: " + count.get() + " entries written to " + filePath; 144 } 145 146 @Override 147 public int getCount() { 148 return count.get(); 149 } 150 151 @Override 152 public void clear() { 153 count.set(0); 154 } 155 156 /** 157 * Closes the file writer. 158 */ 159 @Override 160 public void close() { 161 if (!closed) { 162 closed = true; 163 synchronized (writer) { 164 writer.close(); 165 } 166 } 167 } 168}