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}