Logger.java

package com.example.project.services;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

/**
 * com.example.project.services.Logger for Application and to show up in test and build CI workflow.
 */
public class Logger
{
    private final PrintStream consoleErrorStream;
    private final PrintStream captureErrorStream;
    private ByteArrayOutputStream capturedErrorLogs = new ByteArrayOutputStream();

    private final PrintStream consoleStdOutStream;
    private final PrintStream captureStdOutStream;
    private ByteArrayOutputStream capturedStdOutLogs = new ByteArrayOutputStream();

    private boolean printToConsole = true;

    /**
     * Default constructor writes to console does not capture. For the project files. Use below for unit tests.
     */
    public Logger()
    {
        this.consoleErrorStream = System.err;
        this.captureErrorStream = new PrintStream(capturedErrorLogs);

        this.consoleStdOutStream = System.out;
        this.captureStdOutStream = new PrintStream(capturedStdOutLogs);
    }

    /**
     * Constructor for unit tests. Logger with constructor to input the byte array output stream to write to. (for mocking a log to check get methods.)
     * @param mockCapturedErrorLog byte array to store error logs.
     * @param mockCapturedStdOutLog byte array to store standard output logs.
     */
    public Logger(ByteArrayOutputStream mockCapturedErrorLog, ByteArrayOutputStream mockCapturedStdOutLog) {
        this.setPrintToConsole(false);
        this.consoleErrorStream = System.err;
        this.capturedErrorLogs = mockCapturedErrorLog;
        this.captureErrorStream = new PrintStream(this.capturedErrorLogs);

        this.consoleStdOutStream = System.out;
        this.capturedStdOutLogs = mockCapturedStdOutLog;
        this.captureStdOutStream = new PrintStream(this.capturedStdOutLogs);
    }

    /**
     * @param value if this logger will also print to the console.
     */
    public void setPrintToConsole(boolean value){
        this.printToConsole = value;
    }

    /**
     * @return Gets the error log messages.
     */
    public String getErrorLogs()
    {
        return this.capturedErrorLogs.toString();
    }


    /**
     * @return returns the standard log messages.
     */
    public String getLogs(){ return this.capturedStdOutLogs.toString(); }

    /**
     * Logs an error message to standard error. And adds a newline.
     * <p>
     * The message can include format specifiers like in {@link String#format(String, Object...)}.
     *
     * @param message the error message format string (e.g., "Failed to connect to %s")
     */
    public void logError(String message)
    {
        this.logWithCapture(message, captureErrorStream, consoleErrorStream);
    }

    /**
     * Clears captured logs use in tests teardown.
     */
    public void clearLogs() {
        capturedErrorLogs.reset();
    }

    /**
     * Log message to System.out.
     * @param formattedMessage message.
     */
    public void logMessage(String formattedMessage)
    {
        this.logWithCapture(formattedMessage, captureStdOutStream, consoleStdOutStream);
    }

    private synchronized void logWithCapture(String formattedMessage, PrintStream captureStream, PrintStream consoleStream)
    {
        captureStream.printf(formattedMessage + "%n"); // capture in memory
        if (printToConsole)
        {
            consoleStream.printf(formattedMessage + "%n"); // optional console output
        }
    }
}