# LoggingUtil.java

    /*
     ** Module   : LoggingUtil.java
     ** Abstract : Logging Utilities
     **
     ** Copyright (c) 2023, Golden Code Development Corporation.
     **
     ** -#- -I- --Date-- ----------------------------Description-----------------------------
     ** 001 GBB 20230424 Initial setup
     ** 002 GBB 20230519 Support for disabling rotation.
     ** 003 GBB 20230530 Remove ConfigurationException from canWriteToPath.
     **                  More condensed format for SORTABLE_DATETIME_FORMAT.
     ** 004 GBB 20230602 Add a convenience method to format current datetime.
     ** 005 RAA 20230608 Added a resolveLogFile function that includes the database name.
     ** 006 GBB 20230613 Added support for vars in square brackets in log file name placeholders.
     **                  convertToLevel methods moved to LoggingUtils.
     ** 007 GBB 20230623 Added methods to format numbers/texts with leading symbols/zeros.
     */
    /*
     ** This program is free software: you can redistribute it and/or modify
     ** it under the terms of the GNU Affero General Public 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 Public 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.util.*;

    import java.io.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.*;

    /** Logging utilities. */
    public class LoggingUtil
    {
       /** Format for file names with sortable datetime to the seconds. java.util.Formatter specifiers. */
       public static final String SORTABLE_DATETIME_FORMAT = "%tY%tm%td_%tH%tM%tS";

       /** The placeholder for process id. */
       private static final String PLACEHOLDER_PID = "%pid";

       /** The characters at the start of a placeholder for var. */
       private static final String VAR_PLACEHOLDER_START = "[";

       /** The characters at the end of a placeholder for var. */
       private static final String VAR_PLACEHOLDER_END = "]";

       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(LoggingUtil.class);

       /**
        * Returns the resolved log file name.
        *
        * @param   logFile
        *          File path for the log.
        * @param   processIdSupplier
        *          Supploer for the process Id.
        * @param   processType
        *          Client, server, whatever you like.
        * @param   isRotationDisabled
        *          Flag to indicate rotation has been disabled.
        *
        * @return  The resolved log file name.
        */
       public static String resolveLogFile(String logFile,
                                           Supplier<Long> processIdSupplier,
                                           String processType,
                                           boolean isRotationDisabled)
       {
          return resolveLogFile(logFile,
                                processIdSupplier,
                                null,
                                null,
                                null,
                                processType,
                                null,
                                isRotationDisabled);
       }

       /**
        * Returns the resolved log file name.
        *
        * @param   logFile
        *          File path for the log.
        * @param   processIdSupplier
        *          Supploer for the process Id.
        * @param   processType
        *          Client, server, whatever you like.
        * @param   databaseName
        *          The name of the database in use.
        * @param   isRotationDisabled
        *          Flag to indicate rotation has been disabled.
        *
        * @return  The resolved log file name.
        */
       public static String resolveLogFile(String logFile,
                                           Supplier<Long> processIdSupplier,
                                           String processType,
                                           String databaseName,
                                           boolean isRotationDisabled)
       {
          return resolveLogFile(logFile,
                                processIdSupplier,
                                null,
                                null,
                                null,
                                processType,
                                databaseName,
                                isRotationDisabled);
       }

       /**
        * Returns the resolved log file name.
        *
        * @param   logFile
        *          File path for the log.
        * @param   processIdSupplier
        *          Supploer for the process Id.
        * @param   userOS
        *          Operating system user.
        * @param   userFwd
        *          FWD context user.
        * @param   appServerName
        *          AppServer name.
        * @param   mode
        *          Client, server, whatever you like.
        * @param   databaseName
        *          The name of the database in use.
        * @param   isRotationDisabled
        *          Flag to indicate rotation has been disabled.
        *
        * @return  The resolved log file name.
        */
       public static String resolveLogFile(String logFile,
                                           Supplier<Long> processIdSupplier,
                                           String userOS,
                                           String userFwd,
                                           String appServerName,
                                           String mode,
                                           String databaseName,
                                           boolean isRotationDisabled)
       {
          if (!StringHelper.hasContent(logFile))
          {
             return null;
          }
          if (isRotationDisabled)
          {
             logFile = logFile.replace("_%g", "")
                              .replace("%g", "");
          }
          if (logFile.contains(PLACEHOLDER_PID))
          {
             logFile = logFile.replace(PLACEHOLDER_PID, String.valueOf(processIdSupplier.get()));
          }
          if (mode == null && logFile.contains("_%m"))
          {
             logFile = logFile.replace("_%m", "");
          }
          if (userFwd == null && logFile.contains("_%uf"))
          {
             logFile = logFile.replace("_%uf", "");
          }
          if (userOS == null && logFile.contains("_%uos"))
          {
             logFile = logFile.replace("_%uos", "");
          }
          if (appServerName == null && logFile.contains("_%as"))
          {
             logFile = logFile.replace("_%as", "");
          }
          if (databaseName == null && logFile.contains("_%db"))
          {
             logFile = logFile.replace("_%db", "");
          }
          logFile = logFile.replace("%m", mode != null ? mode : "")
                           .replace("%uf", userFwd != null ? userFwd : "")
                           .replace("%uos", userOS != null ? userOS : "")
                           .replace("%as", appServerName != null ? appServerName : "")
                           .replace("%db", databaseName != null ? databaseName : "");

          if (logFile.contains(VAR_PLACEHOLDER_START) && logFile.contains(VAR_PLACEHOLDER_END))
          {
             String varName;
             while ((varName = getFirstBetween(logFile, VAR_PLACEHOLDER_START, VAR_PLACEHOLDER_END)) != null)
             {
                String toReplace = VAR_PLACEHOLDER_START + varName + VAR_PLACEHOLDER_END;
                String value = null;
                // TODO: receive values for var names from bootstrap
                logFile = logFile.replace(toReplace, value == null ? "" : value);
             }
          }
          return replaceFormatterDateTime(logFile);
       }

       /**
        * Format the current datetime.
        *
        * @return  The formatted datetime.
        */
       public static String formatNow()
       {
          return replaceFormatterDateTime(SORTABLE_DATETIME_FORMAT);
       }

       /**
        * Return the formatted log file path.
        *
        * @param   filePath
        *          File path for the log.
        *
        * @return  The formatted log file path.
        */
       public static String replaceFormatterDateTime(String filePath)
       {
          // non-datetime Formatter specifiers should not be handled at this stage, so escape them
          filePath = filePath.replaceAll("%", "%%");
          filePath = filePath.replaceAll("%%t", "%t");

          int foundTimeSpecifiers = -1;
          int indexOfTimeSpecifier = -1;
          do
          {
             foundTimeSpecifiers++;
             indexOfTimeSpecifier = filePath.indexOf("%t", indexOfTimeSpecifier + 1);
          }
          while (indexOfTimeSpecifier != -1);

          Date[] dateArgs = new Date[foundTimeSpecifiers];
          Arrays.fill(dateArgs, new Date());

          filePath = String.format(filePath, (Object[]) dateArgs);

          return filePath.replaceAll("%%", "%");
       }

       /**
        * Return if the process can write to the file path.
        *
        * @param   filePath
        *          File path for the log.
        *
        * @return  <code>true</code> if the process can write to the file path, <code>false</code> otherwise.
        */
       public static boolean canWriteToPath(String filePath)
       {
          try
          {
             File location = new File(filePath);

             File dir = location.getParentFile();
             // create the dirs to the file
             if (dir != null && !dir.exists())
             {
                if (!dir.mkdirs())
                {
                   LOG.warning("Cannot create dir for log path " + filePath);
                   return false;
                }
             }

             if (location.exists() && (!location.isFile() || !location.canWrite()))
             {
                LOG.warning("The provided log path is not a file or is not writable: " + filePath);
                return false;
             }

             if (!location.exists())
             {
                if (location.createNewFile())
                {
                   if (!location.isFile() || !location.canWrite())
                   {
                      location.delete();
                      LOG.warning("The provided log path is not a file or is not writable: " + filePath);
                      return false;
                   }
                   location.delete();
                }
                else
                {
                   LOG.warning("The provided log path can't be created: " + filePath);
                   return false;
                }
             }
          }
          catch (Exception ex)
          {
             LOG.warning("Exception in setting up the provided log file path " + filePath, ex);
          }
          return true;
       }

       /**
        * Utility method to convert string to java.util.logging.Level. Not the same as
        * {@link Level#parse(String)} that throws exceptions with invalid values.
        *
        * @param    levelString
        *           The string representation of the level.
        *
        * @return   Optional for the parsed java.util.logging.Level.
        */
       public static Optional<Level> convertToLevel(String levelString)
       {
          return java.util.stream.Stream.of(Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO,
                                            Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST,
                                            Level.ALL)
                                        .filter(level -> level.getName().equalsIgnoreCase(levelString))
                                        .findAny();
       }

       /**
        * Utility method to convert integer to java.util.logging.Level. Not the same as
        * {@link Level#parse(String)} that throws exceptions with invalid values.
        *
        * @param    levelInt
        *           The integer representation of the level.
        *
        * @return   Optional for the parsed java.util.logging.Level.
        */
       static Optional<Level> convertToLevel(Integer levelInt)
       {
          return java.util.stream.Stream.of(Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO,
                                            Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST,
                                            Level.ALL)
                                        .filter(level -> level.intValue() == levelInt)
                                        .findAny();
       }

       /**
        * Returns the content between the starting and the ending string.
        *
        * @param   text
        *          The text to search in.
        * @param   start
        *          The wrapper start. To be excluded.
        * @param   end
        *          The wrapper end. To be excluded.
        *
        * @return  The content between the starting and the ending string.
        */
       private static String getFirstBetween(String text, String start, String end)
       {
          int startIndex = text.indexOf(start);
          int endIndex = text.indexOf(end);
          if (startIndex == -1 || endIndex == -1)
          {
             return null;
          }
          return text.substring(startIndex + 1, endIndex);
       }

       /**
        * Format a number by adding leading zeros up to the defined amount of characters.
        *
        * @param   number
        *          The number to format.
        * @param   totalCharsCount
        *          File path for the log.
        *
        * @return  The formatted number with leading zeros.
        */
       public static String addLeadingZeros(Integer number, int totalCharsCount)
       {
          String numberAsString = String.valueOf(number == null ? 0 : number);
          return addLeadingSymbols(numberAsString, totalCharsCount, '0');
       }

       /**
        * Format a text by adding leading symbols up to the defined amount of characters.
        *
        * @param   originalValue
        *          The value to format.
        * @param   totalCharsCount
        *          File path for the log.
        * @param   symbol
        *          The character to use for filling in leading spaces.
        *
        * @return  The formatted text with leading symbols.
        */
       public static String addLeadingSymbols(String originalValue, int totalCharsCount, char symbol)
       {
          int leadingSymbolsCount = totalCharsCount - originalValue.length();
          if (leadingSymbolsCount <= 0)
          {
             return originalValue;
          }
          char[] leadingSymbols = new char[leadingSymbolsCount];
          Arrays.fill(leadingSymbols, symbol);
          return String.valueOf(leadingSymbols) + originalValue;
       }
    }