# CentralLogFormatter.java

    /*
    ** Module   : CentralLogFormatter.java
    ** Abstract : Log formatter for CentralLogger.
    **
    ** Copyright (c) 2023-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------Description-----------------------------
    ** 001 GBB 20230328 Initial setup
    ** 002 GBB 20230613 convertToLevel moved to LoggingUtils.
    ** 003 GBB 20230623 Performance improvements
    ** 004 GBB 20230825 SecurityManager context & session methods calls updated.
    ** 005 TJD 20240117 Do not modify needToInferCaller directly
    ** 006 GBB 20240729 Save log context in a new object and print it later with the message.
    ** 007 GBB 20250403 Checks for sessionId being null and fwdUser being an empty string.
    */
    /*
     ** This program is free software: you can redistribute it and/or modify
     ** it under the terms of the GNU Affero General License as
     ** published by the Free Software Foundation, either version 3 of the
     ** License, or (at your option) any later version.
     **
     ** This program is distributed in the hope that it will be useful,
     ** but WITHOUT ANY WARRANTY; without even the implied warranty of
     ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     ** GNU Affero General License for more details.
     **
     ** You may find a copy of the GNU Affero GPL version 3 at the following
     ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
     **
     ** Additional terms under GNU Affero GPL version 3 section 7:
     **
     **   Under Section 7 of the GNU Affero GPL version 3, the following additional
     **   terms apply to the works covered under the License.  These additional terms
     **   are non-permissive additional terms allowed under Section 7 of the GNU
     **   Affero GPL version 3 and may not be removed by you.
     **
     **   0. Attribution Requirement.
     **
     **     You must preserve all legal notices or author attributions in the covered
     **     work or Appropriate Legal Notices displayed by works containing the covered
     **     work.  You may not remove from the covered work any author or developer
     **     credit already included within the covered work.
     **
     **   1. No License To Use Trademarks.
     **
     **     This license does not grant any license or rights to use the trademarks
     **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
     **     of Golden Code Development Corporation. You are not authorized to use the
     **     name Golden Code, FWD, or the names of any author or contributor, for
     **     publicity purposes without written authorization.
     **
     **   2. No Misrepresentation of Affiliation.
     **
     **     You may not represent yourself as Golden Code Development Corporation or FWD.
     **
     **     You may not represent yourself for publicity purposes as associated with
     **     Golden Code Development Corporation, FWD, or any author or contributor to
     **     the covered work, without written authorization.
     **
     **   3. No Misrepresentation of Source or Origin.
     **
     **     You may not represent the covered work as solely your work.  All modified
     **     versions of the covered work must be marked in a reasonable way to make it
     **     clear that the modified work is not originating from Golden Code Development
     **     Corporation or FWD.  All modified versions must contain the notices of
     **     attribution required in this license.
     */
    package com.goldencode.p2j.util.logging;

    import com.goldencode.p2j.main.*;
    import com.goldencode.p2j.security.SecurityManager;

    import java.io.*;
    import java.text.*;
    import java.util.*;
    import java.util.logging.*;
    import java.util.logging.Formatter;
    import java.util.regex.*;

    /**
     * Extends the java formatter base class used by {@link FileHandler}. Exposes portions of the formatting
     * logic to be reused by other formatters or directly by the CentralLogger. Provides a parse method to
     * convert back formatted text into CentralLogRecord.
     */
    public class CentralLogFormatter
    extends Formatter
    {
       /** The divider between different parts of the log message. */
       static final String LOG_PARTS_DIVIDER = " | ";

       /** The datetime formatter used for each log record. Similar to the one used in 4GL LOG-MANAGER. */
       private static final SimpleDateFormat DATETIME_FORMATTER =
          new SimpleDateFormat("YY/MM/dd HH:mm:ss.SSSXX");

       /** The line separator for the operating system. */
       private static final String LINE_SEPARATOR = System.lineSeparator();

       /**
        * Get a string which uniquely describes the current context. Consists of multiple parts:
        * - the name of the logger mode,
        * - (for clients with initialized native lib) process id,
        * - thread name,
        * - (when security manager has context initialized) session, thread and user id.
        *
        * @param    sessionId
        *           The remote server session ID.
        * @param    userId
        *           The remote server user ID.
        * @param    excludeSMContext
        *           Flag to prevent deadlock on SecurityManager, ref #5703#note-77.
        *
        * @return   Description of the context.
        */
       protected static ContextLogRecord.LogContext describeContext(Integer sessionId, String userId, boolean excludeSMContext)
       {
          ContextLogRecord.LogContext logContext = new ContextLogRecord.LogContext();

          if (ClientCore.isNativeLayerInitialized())
          {
             logContext.setPid(ClientCore.getPid());
          }

          logContext.setThreadName(Thread.currentThread().getName());

          if (sessionId != null || userId != null)
          {
             logContext.setSessionId(sessionId == null || sessionId == 0 ?
                                        null :
                                        LoggingUtil.addLeadingZeros(sessionId, 8));
             logContext.setFwdUser(userId);
          }
          else if (!excludeSMContext)
          {
             SecurityManager sm = SecurityManager.getInstance();
             if (sm != null && SecurityManager.initialized() && sm.contextSm.hasContext())
             {
                sessionId = sm.sessionSm.getSessionId();
                logContext.setSessionId(sessionId == null || sessionId == 0 ?
                                           null :
                                           LoggingUtil.addLeadingZeros(sessionId, 8));
                logContext.setThreadId(LoggingUtil.addLeadingZeros(sm.getThreadId(), 8));
                logContext.setFwdUser(sm.getUserId());
             }
          }

          return logContext;
       }

       /**
        * Convert back formatted text into CentralLogRecord.
        *
        * @param    logLine
        *           The formatted log.
        *
        * @return   CentralLogRecord parsed from the provided text.
        */
       static CentralLogRecord parse(String logLine)
       {
          String[] logRecordParts = logLine.split(Pattern.quote(LOG_PARTS_DIVIDER));
          if (logRecordParts.length < 5)
          {
             return null;
          }

          CentralLogRecord clr = new CentralLogRecord();
          try
          {
             clr.setMillis(DATETIME_FORMATTER.parse(logRecordParts[0].trim()).getTime());
          }
          catch (Throwable ignored)
          {
             // ignore
          }

          clr.setLevel(LoggingUtil.convertToLevel(logRecordParts[1].trim()).orElse(null));
          // concatenates original logger name and source class, method
          clr.setLoggerName(logRecordParts[2].trim());
          // concatenates original context and the actual message
          String msg = "[" + logRecordParts[3].trim() + "] " + logRecordParts[4];
          if (logRecordParts.length > 5)
          {
             String[] msgSegments = Arrays.copyOfRange(logRecordParts, 5, logRecordParts.length - 1);
             msg += String.join("", msgSegments);
          }
          clr.setMessage(msg);
          return clr;
       }

       /**
        * Dump all details of the throwable into the given string buffer.
        * Includes exception type, message, complete stack trace, and chained
        * throwables, if any.
        *
        * @param    sb
        *           The string builder the throwable to be appended to.
        * @param    original
        *           The throwable to dump.
        */
       static void dumpThrowable(StringBuilder sb, Throwable original)
       {
          StringWriter writer = new StringWriter();
          PrintWriter printWriter = new PrintWriter(writer);
          original.printStackTrace(printWriter);
          printWriter.flush();
          sb.append(writer.toString());
       }

       /**
        * Appends the log message to the string builder.
        *
        * @param    sb
        *           The string builder the message to be appended to.
        * @param    record
        *           The log record with the message or the message format and parameters.
        */
       static void dumpMessage(StringBuilder sb, LogRecord record)
       {
          if (!CentralLogger.WRITE_TO_SLF4J_API && record instanceof ContextLogRecord)
          {
             ContextLogRecord.LogContext context = ((ContextLogRecord) record).getLogContext();

             if (context != null)
             {
                boolean isContextAdded = false;

                if (context.getPid() != null)
                {
                   sb.append("PID:").append(context.getPid());
                   isContextAdded = true;
                }

                if (context.getThreadName() != null)
                {
                   sb.append(isContextAdded ? ", " : "").append("ThreadName:").append(context.getThreadName());
                   isContextAdded = true;
                }

                if (context.getSessionId() != null)
                {
                   sb.append(isContextAdded ? ", " : "").append("Session:").append(context.getSessionId());
                   isContextAdded = true;
                }

                if (context.getThreadId() != null)
                {
                   sb.append(isContextAdded ? ", " : "").append("ThreadId:").append(context.getThreadId());
                   isContextAdded = true;
                }

                if (context.getFwdUser() != null && !context.getFwdUser().equals(""))
                {
                   sb.append(isContextAdded ? ", " : "").append("User:").append(context.getFwdUser());
                }

                sb.append(LOG_PARTS_DIVIDER);
             }
          }

          Object[] params = record.getParameters();
          if (params == null || params.length == 0)
          {
             sb.append(record.getMessage());
             return;
          }
          sb.append(String.format(record.getMessage(), params));
       }

       /**
        * Transforms the log record to string. A divider separates the different parts:
        * - datetime,
        * - log level,
        * - logger name,
        * - (optional) source class name, source method name,
        * - message (no divider after it),
        * - (optional) throwable.
        *
        * @param    record
        *           The log record.
        *
        * @return   The string representation of the log record.
        */
       @Override
       public String format(LogRecord record)
       {
          StringBuilder sb = new StringBuilder(DATETIME_FORMATTER.format(new Date(record.getMillis())));
          sb.append(LOG_PARTS_DIVIDER);
          sb.append(LoggingUtil.addLeadingSymbols(record.getLevel().toString(), 7, ' '));
          sb.append(LOG_PARTS_DIVIDER);

          sb.append(record.getLoggerName());

          String sourceClassName = record.getSourceClassName();
          if (sourceClassName != null)
          {
             sb.append(" [").append(sourceClassName);
             String sourceMethodName = record.getSourceMethodName();
             if (sourceMethodName != null && !sourceMethodName.trim().isEmpty())
             {
                sb.append(".").append(sourceMethodName).append("()");
             }
             sb.append("]");
          }
          sb.append(LOG_PARTS_DIVIDER);

          dumpMessage(sb, record);
          sb.append(LINE_SEPARATOR);

          if (record.getThrown() != null)
          {
             dumpThrowable(sb, record.getThrown());
          }

          return sb.toString();
       }
    }