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;
   }
}