LegacyLogManagerImpl.java
/*
** Module : LegacyLogManagerImpl.java
** Abstract : Implementation for the LOG-MANAGER system handles.
**
** Copyright (c) 2014-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20141028 Created initial version.
** 002 CA 20220605 Added an experimental implementation of the logging, which just dumps the info the
** server's log.
** 003 GBB 20230313 Implementation of the base functionality.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 GBB 20230523 Fixes LOG-MANAGER entry types parsing when no level applied.
** Handles properly entry types value "::".
** Changes entry type validation checks throwing errors to showing warnings.
** 006 GBB 20230608 Multiple methods modified to be exposed as package-private static to be reused by client
** LOG-MANAGER service. Remote object of client LOG-MANAGER service used to perform
** file operations on log files client-side when the server-side filesystem config is
** disabled. Entry types to be resolved case-insensitive. Appserver warnings added.
** 007 GBB 20230609 Fixes NPEs when configs not present.
** 008 TT 20230915 Migrated fileSize and numLogFiles from String to Integer.
** 009 GBB 20240311 'isEnabled' flag needs to be set to false when the log file is closed.
** 010 GBB 20241010 Removing redundant constants. Exposing constants for the default logging level,
** threshold and number of log files.
** 011 ICP 20250129 Used logical and character constants to leverage caches instances, fixed javadoc.
** 012 GBB 20250307 Fix finding the procedure name from the callstack. Defaults moved to LegacyLogOps.
** Log entry types full support. Enums out to the package.
** Multi-session appserver specifics added.
*/
/*
** 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;
import java.io.*;
import java.nio.file.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
import java.util.regex.*;
import java.util.stream.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.net.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.appserver.*;
import com.goldencode.p2j.util.logging.*;
import static com.goldencode.util.StringHelper.*;
/** Implementation for the LOG-MANAGER system handle. */
@LegacyResource(resource = LegacyResource.PSEUDO_WIDGET)
public class LegacyLogManagerImpl
extends HandleResource
implements LegacyLogManager
{
/** Default logging level for handle execution of WRITE-MESSAGE(). */
private static final int HANDLE_CALL_LOGGING_LEVEL = LoggingLevel.ERROR.toInt();
/** Number of characters of subsys column. */
private static final int SUBSYS_CHARS_COUNT = 15;
/** Formatter for date and time, used by WRITE-MESSAGE(). */
private static final SimpleDateFormat DATETIME_FORMATTER =
new SimpleDateFormat("YY/MM/dd@HH:mm:ss.SSSXX");
/** The minimum log file size threshold for roll over in bytes. */
private static final int MIN_LOG_FILE_THRESHOLD_BYTES = 500_000;
/** The maximum number of log files for roll over. */
private static final int MAX_NUM_LOG_FILES = 999_999;
/** A logger to write messages to server's log. */
private static final CentralLogger LOG = CentralLogger.get(LegacyLogManagerImpl.class);
/** Default entry types for ABL clients. */
private static final String DEFAULT_TYPES = LoggingEntryType.Messages.toString();
/** A reference to the queue that handles writing log records. */
private final LegacyLogWriteExecutor writeExecutor;
/** A flag to indicate execution of the method initialize has completed. */
private final AtomicBoolean isInitialized = new AtomicBoolean(false);
/** Flag to indicate if LOG-MANAGER is enabled by configs. */
private AtomicBoolean isEnabled = new AtomicBoolean(false);
/** Max number of characters supported in one message. */
private int maxInputCharacters = 15_000;
/** A flag to indicate if running in an appserver. */
private boolean isAppserver;
/** The client process id. */
private long clientProcessId = 0L;
/** The log file path for clients or servers (AppServer and WebSpeed). Absolute path. */
private File logFile = null;
/** The file writer. */
private FileWriter logWriter;
/** The default entry type - logging level pairs. */
private final Map<LoggingEntryType, Integer> activatedTypeLevelPairs = new HashMap<>();
/** The default logging level. */
private int loggingLevel = LegacyLogOps.DEFAULT_LOGGING_LEVEL;
/** The default logging threshold. */
private int logThreshold = LegacyLogOps.DEFAULT_THRESHOLD;
/** The default number of logging files. */
private int numLogFiles = LegacyLogOps.DEFAULT_NUM_FILES;
/** Client service. */
private LegacyLogManagerClientService client;
/** Client session. */
private Session session;
/** Counter for client-side log file size. */
private AtomicInteger remoteFileSize = new AtomicInteger(0);
/**
* Package private constructor to create an instance of a LOG-MANAGER.
*
* @param writeExecutor
* Reference to the executor that actually writes log messages.
*/
LegacyLogManagerImpl(LegacyLogWriteExecutor writeExecutor)
{
this.writeExecutor = writeExecutor;
LOG.log(Level.FINE, "New instance created for LOG-MANAGER");
}
/**
* Validates and sets configs coming from command line.
*
* @param configs
* Configs.
*/
@Override
public void initialize(LegacyLogManagerConfigs configs)
{
if (isInitialized.get())
{
return;
}
session = SessionManager.get().getSession();
if (!configs.isServerSideFileSystem())
{
client = LogicalTerminal.getInstance().getLegacyLogManagerClient();
}
initialize(configs.getLogFile(), configs.getLevelString(), configs.getEntryTypes(),
configs.getFileSize(), configs.getNumLogFiles(), configs.getProcessId(),
configs.getMaxInputCharacters(), configs.isAppserver(),
configs.isLoggerInitialized());
configs.setLoggerInitialized();
}
/**
* Sets configs coming from command line.
* They need to be set at once, because there are dependencies between them.
* The recipe for initialization requires the order of setters to be kept in a strict order.
* @param logFile
* File path for the log.
* @param levelString
* Default logging level.
* @param entryTypes
* List of comma-separated log entry types.
* @param fileSize
* File size in bytes used as threshold for file roll over.
* @param numLogFiles
* Max number of log files.
* @param clientProcessId
* The id of the client process.
* @param maxInputCharacters
* The max number of characters.
* @param isAppServerLoggerInitialized
* A flag to indicate if the appserver logger has already been initialized in a different.
*/
private synchronized void initialize(String logFile,
String levelString,
String entryTypes,
Integer fileSize,
Integer numLogFiles,
long clientProcessId,
int maxInputCharacters,
boolean isAppserver,
boolean isAppServerLoggerInitialized)
{
this.clientProcessId = clientProcessId;
this.isAppserver = isAppserver;
if (maxInputCharacters > 0)
{
this.maxInputCharacters = maxInputCharacters;
}
if (hasContent(levelString))
{
int level;
try
{
level = Integer.parseInt(levelString);
}
catch (Exception e)
{
recordOrThrowError(11996, "The -logginglevel parameter requires a numeric " +
"argument.", false);
LOG.log(Level.CONFIG, "Logging level cannot be parsed. Initialization stops.");
return;
}
this.setLoggingLevel(level);
if (this.loggingLevel <= LoggingLevel.OFF.toInt())
{
LOG.log(Level.CONFIG, "Logging level wasn't set. Initialization stops.");
return;
}
}
if (fileSize != null && !this.setLogThreshold(fileSize))
{
LOG.log(Level.CONFIG, "There was an issue with log file threshold value. Initialization stops.");
return;
}
if (numLogFiles != null && !this.setNumLogFiles(numLogFiles))
{
LOG.log(Level.CONFIG, "There was an issue with log files number. Initialization stops.");
return;
}
if (!hasContent(logFile) &&
(hasContent(levelString) || fileSize != null || numLogFiles != null || hasContent(entryTypes)))
{
recordOrThrowError( 11068, "Parameters for logging were specified but -clientlog was not set. " +
"Logging parameters will be ignored. Specify -logginglevel 0 if you want to keep logging " +
"disabled at startup.", false);
LOG.log(Level.CONFIG, "Log file name not specified. Initialization stops.");
return;
}
this.setLogFileName(false, logFile);
this.setLogEntryTypes(hasContent(entryTypes) ?
entryTypes :
isAppserver ? LegacyLogOps.DEFAULT_APPSERVER_TYPES : DEFAULT_TYPES);
if (!isAppServerLoggerInitialized)
{
writeSystemHeader();
}
isInitialized.set(true);
LOG.log(Level.CONFIG, "LOG-MANAGER initialized:" +
" clientProcessId=" + clientProcessId +
" logFile=" + logFile +
" loggingLevel=" + loggingLevel +
" logThreshold=" + logThreshold +
" numLogFiles=" + this.numLogFiles +
" activatedTypeLevelPairs=" + activatedTypeLevelPairs);
}
/**
* Provides the comma-separated list of currently valid entry types that can be assigned to
* the {@link #setLogEntryTypes}.
*
* @return The list of valid log entry types.
*/
@Override
public character getEntryTypesList()
{
String supportedEntryTypesList = String.join(",", LoggingEntryType.get4GLNames());
return new character(supportedEntryTypesList);
}
/**
* Provides the comma-separated list of entry types that are being actively used for logging.
*
* @return The list of active log entry types.
*/
@Override
public character getLogEntryTypes()
{
if (activatedTypeLevelPairs.isEmpty())
{
return new character();
}
String entryTypes =
activatedTypeLevelPairs.entrySet()
.stream()
.map(entrySet -> {
String typeLevelPair = entrySet.getKey().getName();
if (entrySet.getValue() != LoggingLevel.UNDEFINED.toInt())
{
typeLevelPair = typeLevelPair + ":" + entrySet.getValue();
}
return typeLevelPair;
})
.collect(Collectors.joining(","));
return new character(entryTypes);
}
/**
* Modifies the list of log entry types that are being actively used for logging.
*
* @param types
* The comma-separated list of active entry types.
*/
@Override
public synchronized void setLogEntryTypes(String types)
{
LOG.log(Level.CONFIG, "Log entry types set to " + types);
activatedTypeLevelPairs.clear();
if (!hasContent(types))
{
resetEnabled();
return;
}
String[] entryTypeInfos = types.split(",");
for (String entryTypeInfo : entryTypeInfos)
{
String[] typeLevelPair = entryTypeInfo.split(":");
if (typeLevelPair.length == 0)
{
recordOrShowWarning(11069, "Ignoring unknown log entry type: " + entryTypeInfo, false);
continue;
}
String typeName = typeLevelPair[0].trim();
LoggingEntryType foundType = LoggingEntryType.get(typeName);
if (foundType == null || (isAppserver && foundType == LoggingEntryType.Messages))
{
recordOrShowWarning(11069, "Ignoring unknown log entry type: " + entryTypeInfo, false);
continue;
}
if (typeLevelPair.length < 2)
{
if (loggingLevel == LoggingLevel.UNDEFINED.toInt())
{
recordOrShowWarning(11072,
"Logging level greater than 1 must be specified either for " +
typeName + " or for all types by using -logginglevel.",
false);
}
else
{
activatedTypeLevelPairs.put(foundType, LoggingLevel.UNDEFINED.toInt());
}
}
else
{
try
{
int levelNumber = Integer.parseInt(typeLevelPair[1].trim());
if (levelNumber < LoggingLevel.BASIC.toInt())
{
recordOrShowWarning(11071,
"Logging level for " + typeName + " must be " +
LoggingLevel.BASIC.toInt() + " or higher",
false);
continue;
}
activatedTypeLevelPairs.put(foundType, levelNumber);
}
catch (Exception e)
{
LOG.log(Level.CONFIG, "Cannot parse to int entry type logging level " + typeLevelPair[1], e);
recordOrShowWarning(11071, "Logging level for " + typeName + " must be 2 or higher", false);
}
}
}
resetEnabled();
if (isInitialized.get())
{
writeMessage(activatedTypeLevelPairs.isEmpty() ?
"No log entry types are activated" :
"Log entry types activated: " +
activatedTypeLevelPairs.keySet()
.stream()
.map(LoggingEntryType::name)
.collect(Collectors.joining(","))
);
}
}
/**
* Modifies the list of log entry types that are being actively used for logging.
*
* @param types
* The comma-separated list of active entry types.
*/
@Override
public synchronized void setLogEntryTypes(character types)
{
setLogEntryTypes(types == null || types.isUnknown() ? null : types.toStringMessage());
}
/**
* Obtains the file path to which log entries and stack traces are written.
*
* @return The log file path.
*/
@Override
public character getLogFileName()
{
return logFile == null ? character.UNKNOWN : character.of(logFile.getPath());
}
/**
* Assigns the file path to which log entries and stack traces are written.
*
* @param logFileConfig
* The log file path.
*/
@Override
public synchronized void setLogFileName(String logFileConfig)
{
setLogFileName(true, logFileConfig);
}
/**
* Assigns the filename to which log entries and stack traces are written.
*
* @param filePath
* The log file path.
*/
@Override
public synchronized void setLogFileName(character filePath)
{
setLogFileName(filePath == null || filePath.isUnknown() ? null : filePath.toStringMessage());
}
/**
* Obtains the default logging level for all logging categories.
*
* @return The logging level.
*/
@Override
public integer getLoggingLevel()
{
return new integer(loggingLevel);
}
/**
* Assigns the default logging level for all logging categories.
* <p>
* There are 5 valid levels:
* <p>
* <ul>
* <li> 0 - no logging
* <li> 1 - only errors
* <li> 2 - basic, each entry type determines its own output but all types will generate
* some output
* <li> 3 - verbose
* <li> 4 - extended
* </ul>
*
* @param level
* The logging level to use by default.
*/
@Override
public synchronized void setLoggingLevel(int level)
{
if (isInitialized.get() && logFile == null)
{
recordOrThrowError(11078, "Cannot set attribute LOGGING-LEVEL because log file name was not " +
"specified at startup.", true);
return;
}
if (level < LoggingLevel.OFF.toInt())
{
recordOrThrowError(5049, "The -logginglevel parameter has too many digits.", false);
return;
}
this.loggingLevel = Math.min(level, LoggingLevel.MAX.toInt());
// resets all levels for enabled entry types
activatedTypeLevelPairs.replaceAll((k, v) -> loggingLevel);
resetEnabled();
if (this.logWriter != null)
{
// only after initialization
writeMessage("Logging level set to = " + this.loggingLevel);
}
}
/**
* Assigns the default logging level for all logging categories.
* <p>
* There are 5 valid levels:
* <p>
* <ul>
* <li> 0 - no logging
* <li> 1 - only errors
* <li> 2 - basic, each entry type determines its own output but all types will generate
* some output
* <li> 3 - verbose
* <li> 4 - extended
* </ul>
*
* @param level
* The logging level to use by default.
*/
@Override
public synchronized void setLoggingLevel(integer level)
{
if (level == null)
{
return;
}
setLoggingLevel(level.isUnknown() ? 0 : level.intValue());
}
/**
* Obtains the configured log file size in bytes at which the log file will be renamed and a
* new log file started.
*
* @return The maximum log file size.
*/
@Override
public integer getLogThreshold()
{
return new integer(logThreshold);
}
/**
* Obtains the configured number of log files that will be stored in total (including both
* backed up log files and the current log).
*
* @return The maximum number of log files.
*/
@Override
public integer getNumLogFiles()
{
return new integer(numLogFiles);
}
/**
* Deletes all messages from the currently open log file or deletes all log files in the sequence if
* rotation is enabled. At the end of the operation a new log file is open and system messages showing
* LOG-MANAGER configs are printed.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public synchronized logical clearLog()
{
if (isAppserver)
{
recordOrShowWarning(14331, "CLEAR-LOG() invalid for WebSpeed or AppServer", false);
return new logical(false);
}
if (logFile == null || (client == null && logWriter == null))
{
recordOrShowWarning( 14333, "Cannot clear log because there is no log file open", false);
return new logical(false);
}
String originalLogFileName;
if (isRotationEnabled())
{
String noSequenceName = new SequencedFileNameMatcher(logFile).getNoSequenceName();
originalLogFileName = new File(logFile.getParent(), noSequenceName).getPath();
}
else
{
originalLogFileName = logFile.getPath();
}
if (client != null)
{
remoteFileSize.set(0);
logFile = null;
boolean isSuccess = session.isRunning() && client.clear();
setLogFileName(false, originalLogFileName);
return new logical(isSuccess);
}
if (!isRotationEnabled())
{
try
{
logFile.delete();
logFile = null;
setLogFileName(false, originalLogFileName);
return new logical(true);
}
catch (Exception e)
{
return new logical(false);
}
}
close(false, false);
boolean isSuccess = deleteSequence(logFile);
setLogFileName(false, originalLogFileName);
return new logical(isSuccess);
}
/**
* Closes the currently open log file. Additional logging will not be possible after this
* operation.
* <p>
* This method should not be used as a manual call, but only automatically from procedure execution.
* Use {@link #close(boolean,boolean)} instead.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical closeLog()
{
return close(true, true);
}
/**
* Closes the currently open log file. Additional logging will not be possible after this
* operation.
*
* @param isProcedureCall
* Flag to identify the method is called by the execution of a converted 4GL procedure.
* @param closeWriteExecutor
* Flag to identify the manager is not going to use {@link #writeExecutor} any more. In case
* of file being rotated or {@link #clearLog()} called, this should be <code>false</code>.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public synchronized logical close(boolean isProcedureCall, boolean closeWriteExecutor)
{
if (isAppserver && isProcedureCall)
{
recordOrShowWarning(14331, "CLOSE-LOG() invalid for WebSpeed or AppServer", false);
return new logical(false);
}
isEnabled.set(false);
if (client != null)
{
logFile = null;
remoteFileSize.set(0);
if (!session.isRunning())
{
return new logical(true);
}
if (isProcedureCall && loggingLevel > LoggingLevel.OFF.toInt())
{
client.write(createLogLine(HANDLE_CALL_LOGGING_LEVEL,
"----------",
"Log file closed at user's request",
Thread.currentThread().getId(),
new Date(),
LoggingExecEnv.DEFAULT.toString()));
}
return new logical(client.close());
}
if (logWriter == null)
{
return new logical(true);
}
try
{
if (isProcedureCall && loggingLevel > LoggingLevel.OFF.toInt())
{
writeLocal(createLogLine(HANDLE_CALL_LOGGING_LEVEL,
"----------",
"Log file closed at user's request",
Thread.currentThread().getId(),
new Date(),
LoggingExecEnv.DEFAULT.toString()));
}
logWriter.close();
logWriter = null;
if (closeWriteExecutor)
{
writeExecutor.close(clientProcessId, logFile.getAbsolutePath(), isRotationEnabled());
logFile = null;
}
return new logical(true);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Not able to close log file writer for " + logFile.getAbsolutePath(), e);
return new logical(false);
}
}
/**
* This object is always valid for use. It might output an error message if its state is not prepared
* for logging, but that is the expected behavior.
*
* @return Always <code>true</code>.
*/
@Override
public boolean valid()
{
return true;
}
/**
* Returns <code>true</code> if logging is enabled, <code>false</code> otherwise.
*
* @return See above.
*/
@Override
public boolean isLoggingEnabled()
{
return isEnabled.get();
}
/**
* Write a user defined message to the currently open log file using the default subsystem name "APPL".
* The method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a subsystem,
* that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(String msg)
{
return writeMessage(msg, LoggingSubsys.DEFAULT.toString());
}
/**
* Write a user defined message to the currently open log file using the default subsystem name "APPL".
* The method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a subsystem,
* that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(character msg)
{
return writeMessage(msg, new character(LoggingSubsys.DEFAULT.toString()));
}
/**
* Write a user defined message to the currently open log file using the given subsystem name. The
* method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a
* subsystem, that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
* @param subsys
* Subsystem name (up to 10 characters will be used).
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(String msg, String subsys)
{
return writeMessage(msg,
null,
null,
subsys,
null,
HANDLE_CALL_LOGGING_LEVEL,
Thread.currentThread().getId(),
false);
}
/**
* Write a user defined message to the currently open log file using the given subsystem name. The
* method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a
* subsystem, that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
* @param subsys
* Subsystem name (up to 10 characters will be used).
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(character msg, String subsys)
{
String message = msg == null || msg.isUnknown() ? "?" : msg.toStringMessage();
return writeMessage(message,
null,
null,
subsys,
null,
HANDLE_CALL_LOGGING_LEVEL,
Thread.currentThread().getId(),
false);
}
/**
* Write a user defined message to the currently open log file using the given subsystem name. The
* method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a
* subsystem, that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
* @param subsys
* Subsystem name (up to 10 characters will be used).
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(String msg, character subsys)
{
String subsystem = subsys == null || subsys.isUnknown() ?
LoggingSubsys.DEFAULT.toString() :
subsys.toStringMessage();
return writeMessage(msg,
null,
null,
subsystem,
null,
HANDLE_CALL_LOGGING_LEVEL,
Thread.currentThread().getId(),
false);
}
/**
* Write a user defined message to the currently open log file using the given subsystem name. The
* method is called directly as a manual WRITE-MESSAGE, instead of an auto logging from a
* subsystem, that's why the logging level is set to {@link #HANDLE_CALL_LOGGING_LEVEL}.
* <p>
* Should be used only automatically by the converted code or LOG-MANAGER itself.
*
* @param msg
* The message to write.
* @param subsys
* Subsystem name (up to 10 characters will be used).
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(character msg, character subsys)
{
String message = msg == null || msg.isUnknown() ? "?" : msg.toStringMessage();
String subsystem = subsys == null || subsys.isUnknown() ?
LoggingSubsys.DEFAULT.toString() :
subsys.toStringMessage();
return writeMessage(message, subsystem);
}
/**
* Write an automated message to the currently open log file using the given subsystem name.
*
* @param msg
* The message to write.
* @param entryType
* The type of the entry.
* @param execEnv
* The execution environment.
* @param subsys
* Subsystem name (up to 10 characters will be used).
* @param logLevel
* The log level.
*
* @return <code>true</code> if the operation succeeded, or <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(String msg,
LoggingEntryType entryType,
LoggingExecEnv execEnv,
LoggingSubsys subsys,
LoggingLevel logLevel)
{
return writeMessage(msg,
entryType,
execEnv,
subsys == null ? null : subsys.toString(),
null,
logLevel.toInt(),
Thread.currentThread().getId(),
false);
}
/**
* Write an automated message from the given subsystem to the currently open log file.
* <p>
* To be used for custom calls / auto logging of entry types.
*
* @param msg
* The message to write.
* @param entryType
* The type of the entry.
* @param execEnv
* The execution environment.
* @param subsys
* Subsystem name (up to 10 characters will be used).
* @param callStack
* The call stack.
* @param logLevel
* The logging level of the message.
* @param threadId
* The client thread id or server thread id for AppServer and WebSpeed.
*
* @return logical <code>true</code> if the operation succeeded, or logical <code>false</code>
* otherwise.
*/
@Override
public logical writeMessage(String msg,
LoggingEntryType entryType,
LoggingExecEnv execEnv,
LoggingSubsys subsys,
LoggingLevel logLevel,
String[] callStack,
long threadId)
{
return writeMessage(msg,
entryType,
execEnv,
subsys == null ? null : subsys.toString(),
callStack,
logLevel.toInt(),
threadId,
true);
}
/**
* Write a user defined message from a handle WRITE-MESSAGE call or an automated message to the
* currently open log file using the given subsystem name.
* <p>
* To be used for custom calls / auto logging of entry types.
*
* @param msg
* The message to write.
* @param entryType
* The type of the entry. Can be <code>null</code>.
* @param execEnv
* The execution environment abbr.
* @param subsys
* Subsystem name (up to 10 characters will be used). It may match the entry type, or it can
* be a custom one with calls to WRITE-MESSAGE.
* @param callStack
* The call stack.
* @param msgLogLevel
* The logging level of the message.
* @param threadId
* The client thread id or server thread id for AppServer and WebSpeed.
* @param isAutoLogging
* A flag to indicate if auto logging from a subsystem or a user call from converted procedure.
*
* @return logical <code>true</code> if the operation succeeded, or logical <code>false</code>
* otherwise.
*/
private synchronized logical writeMessage(String msg,
LoggingEntryType entryType,
LoggingExecEnv execEnv,
String subsys,
String[] callStack,
int msgLogLevel,
long threadId,
boolean isAutoLogging)
{
if (!isEnabled.get() && isAutoLogging)
{
return logical.FALSE;
}
if (client == null && logWriter == null)
{
recordOrThrowError(14332, "Cannot write message to log, as there is no log open", false);
return logical.FALSE;
}
if (loggingLevel == LoggingLevel.OFF.toInt() || loggingLevel < msgLogLevel)
{
return logical.FALSE;
}
if (entryType != null)
{
if (!activatedTypeLevelPairs.containsKey(entryType))
{
return logical.FALSE;
}
Integer maxLevelForEntryType = activatedTypeLevelPairs.get(entryType);
if (maxLevelForEntryType == LoggingLevel.OFF.toInt() || maxLevelForEntryType < msgLogLevel)
{
return logical.FALSE;
}
}
if (msg == null)
{
msg = "?";
}
else if (msgLogLevel >= LoggingLevel.VERBOSE.toInt())
{
// TODO: confirm it's valid for all logs level 3 or above
msg = "TRACE: " + msg;
}
if (msg.length() > maxInputCharacters)
{
recordOrThrowError(135, "More than " + maxInputCharacters + " characters in a single " +
"statement--use -inp parm.", true);
return logical.FALSE;
}
Date date = new Date();
if (subsys == null)
{
subsys = isAppserver ? LoggingSubsys.AS.toString() : LoggingSubsys.DEFAULT.toString();
}
String fullExecEnv;
if (execEnv == null)
{
if (isAppserver)
{
// TODO: classic?
MultiSessionAppserverManager msaManager = MultiSessionAppserverManager.getInstance();
Integer sessionId = msaManager.getSessionId();
fullExecEnv = LoggingExecEnv.AS +
(msaManager.isMsaContext() && sessionId != null ? "-" + sessionId : "");
}
else
{
fullExecEnv = LoggingExecEnv.DEFAULT.toString();
}
}
else
{
fullExecEnv = execEnv.toString();
}
String logRecord = createLogRecord(msg, subsys, callStack, msgLogLevel, threadId, date, fullExecEnv);
long fileSize = client != null ? remoteFileSize.get() : logFile.length();
int logRecordSize = logRecord.getBytes().length;
if (isRotationEnabled() && fileSize + logRecordSize > logThreshold)
{
rotateLogFile();
}
if (client == null)
{
return logical.of(writeLocal(logRecord));
}
return logical.of(writeRemote(logRecord, isAutoLogging));
}
/**
* Formats the input data as a log record.
*
* @param msg
* The message to write.
* @param subsys
* Subsystem name (up to 10 characters will be used).
* @param callStack
* The call stack.
* @param logLevel
* The logging level of the message.
* @param threadId
* The client thread id or server thread id for AppServer and WebSpeed.
* @param date
* The date of the log record.
* @param execEnv
* The execution environment.
*
* @return The formatted log record.
*/
private String createLogRecord(String msg,
String subsys,
String[] callStack,
int logLevel,
long threadId,
Date date,
String execEnv)
{
if (callStack != null && callStack.length > 0)
{
if (SessionUtils.isDebugAlert().booleanValue())
{
StringBuilder msgBuilder = new StringBuilder();
msgBuilder.append(createLogLine(logLevel, subsys, msg, threadId, date, execEnv));
msgBuilder.append(createLogLine(logLevel,
subsys,
"** ABL Debug-Alert Stack Trace **",
threadId,
date,
execEnv));
for (int i = 0; i < callStack.length; i++)
{
String callStackEntry = callStack[i];
String callStackPrefix = i == 0 ? "--> " : " ";
msgBuilder.append(createLogLine(logLevel,
subsys,
callStackPrefix + callStackEntry,
threadId,
date,
execEnv));
}
return msgBuilder.toString();
}
msg = "(Procedure: '" + callStack[callStack.length - 1] + "') " + msg;
}
return createLogLine(logLevel, subsys, msg, threadId, date, execEnv);
}
/** Sets the isEnabled flag. */
private void resetEnabled()
{
isEnabled.set(logFile != null &&
loggingLevel > LoggingLevel.OFF.toInt() &&
activatedTypeLevelPairs.size() > 0);
}
/**
* Checks if the folder exists and if the JVM process has rights to write to it.
*
* @param file
* The file to check for.
*
* @return <code>true</code> if the folder is writable, <code>false</code> otherwise.
*/
static boolean isWritable(File file)
{
if (file == null)
{
return false;
}
if (file.exists() && file.isFile() && file.canWrite())
{
return true;
}
try
{
File folder = file.getAbsoluteFile().getParentFile();
return folder.exists() && folder.canWrite();
}
catch (SecurityException e)
{
return false;
}
}
/**
* Adds a dot and a sequence number to the file name before the file extension. The sequence number is six
* digits with leading zeros in the range 000001 - 999999. Traverses log file directory to find all
* files in the sequence. The first consecutive numbers found are used to determine the current sequence
* number. If the file is full based on the {@link #logThreshold} value, the method increments the last
* sequence number or uses 1 if the end of the allowed range 999999 is reached.
*
* @param logFilePath
* The log file without sequence number.
* @param logThreshold
* The log file size threshold for rolling over to a new file.
*
* @return The log file with updated name that includes the sequence number of the current last log
* file.
*/
static File makeSequencedLogFile(String logFilePath, int logThreshold)
{
File logFile = new File(logFilePath);
File dir = logFile.getParentFile(); // can be null for relative paths
FileName fn = new GenericFileName(logFile.getName());
int lastLogFileNumber = findLastLogFileNumber(logFile.getAbsoluteFile().getParentFile(),
fn.getPrefix(),
fn.getExt());
if (lastLogFileNumber == 0)
{
String sequencedName = formatFileName(fn, 1);
return new File(dir, sequencedName);
}
File lastLogFile = new File(dir, formatFileName(fn, lastLogFileNumber));
if (lastLogFile.length() < logThreshold)
{
return lastLogFile;
}
String currentSequencedName = formatFileName(fn, nextSequenceNumber(lastLogFileNumber));
return new File(dir, currentSequencedName);
}
/**
* Removes the sequenced log files with the same name in the same folder that exceed the max count of
* {@link #numLogFiles}, while preserving the current log file and of its predecessors.
*/
static void cleanupRotation(File logFile, int numLogFiles)
{
logFile = logFile.getAbsoluteFile();
SequencedFileName sequencedFileName = new SequencedFileName(logFile.getName());
if (!sequencedFileName.isValid)
{
return;
}
SequencedFileNameMatcher matcher =
new SequencedFileNameMatcher(sequencedFileName.getPrefix(), sequencedFileName.getExt());
try
{
List<Integer> logFileNumbers =
Files.walk(logFile.getParentFile().toPath(), 1)
.map(Path::toFile)
.filter(File::isFile)
.map(file -> matcher.findSequenceNumber(file.getName()))
.filter(number -> number > 0 && number <= MAX_NUM_LOG_FILES)
.sorted()
.collect(Collectors.toList());
if (logFileNumbers.size() <= numLogFiles)
{
return;
}
int filesToDeleteCount = logFileNumbers.size() - numLogFiles;
int indexOfCurrentLogFile = logFileNumbers.indexOf(sequencedFileName.sequenceNumberInt);
while (filesToDeleteCount > 0)
{
// first deletes next and then starts from the first
int fileNumber = indexOfCurrentLogFile < logFileNumbers.size() - 1 ?
logFileNumbers.remove(indexOfCurrentLogFile + 1) :
logFileNumbers.remove(0);
String fileName = formatFileName(sequencedFileName, fileNumber);
new File(logFile.getParentFile(), fileName).delete();
filesToDeleteCount--;
}
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Not able to clear all log files.", e);
}
}
/**
* Deletes all files in the sequence in the same folder as the provided log file.
*
* @param logFile
* The log file to base the sequence finding on.
*
* @return <code>true</code> if all files in the sequence successfully deleted,
* <code>false</code> otherwise.
*/
static boolean deleteSequence(File logFile)
{
SequencedFileNameMatcher matcher = new SequencedFileNameMatcher(logFile);
boolean isSuccess = false;
try
{
isSuccess = Files.walk(logFile.getAbsoluteFile().getParentFile().toPath(), 1)
.map(Path::toFile)
.filter(file -> file.isFile() &&
matcher.matches(file.getName()))
.allMatch(File::delete);
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Not able to clear all log files.", e);
}
return isSuccess;
}
/**
* Get the next number in the sequence valid range.
*
* @param lastNumber
* The previous number in the sequence.
*
* @return The next number in the sequence.
*/
private static int nextSequenceNumber(int lastNumber)
{
return lastNumber == 999999 ? 1 : lastNumber + 1;
}
/**
* Concatenates the file name, sequence and extension. Number sequence is formatted with 6 digits
* (leading zeros are added if needed).
*
* @param fileName
* The file name object wrapper for the name prefix (name without extension and sequence number)
* and the extension itself.
* @param sequenceNumber
* The sequence number.
*
* @return The formatted file name.
*/
private static String formatFileName(FileName fileName, int sequenceNumber)
{
int leadingZerosCount = 6 - String.valueOf(sequenceNumber).length();
String leadingZeros = new String(new char[leadingZerosCount]).replace('\0', '0');
String currentNumberFormatted = leadingZeros + sequenceNumber;
return fileName.getExt() == null ?
fileName.getPrefix() + "." + currentNumberFormatted :
fileName.getPrefix() + "." + currentNumberFormatted + "." + fileName.getExt();
}
/**
* Format process or thread id. Should take up at least 6 characters including leading zeros.
*
* @param id
* The process or thread id.
*
* @return The formatted id.
*/
private static String formatSystemId(long id)
{
String idString = String.valueOf(id);
if (idString.length() >= 6)
{
return idString;
}
int leadingZerosCount = 6 - idString.length();
String leadingZeros = new String(new char[leadingZerosCount]).replace("\0", "0");
return leadingZeros + idString;
}
/**
* This should reflect the 4GL logic to find the last file that has been written to. Traverses log file
* directory to find all files in the sequence. The last gap in the numbers found are used to determine
* the current sequence number. The last consecutive number (of that preceding sub-sequence) is returned.
*
* @param dir
* The directory to search for log files.
* @param nameNoExt
* The name of the file without extension and sequence.
* @param ext
* The extension of the file.
*
* @return The number in the sequence of the last log file.
*/
private static int findLastLogFileNumber(File dir, String nameNoExt, String ext)
{
File[] allDirFiles = dir.listFiles();
if (allDirFiles == null || allDirFiles.length == 0)
{
return 0;
}
Arrays.sort(allDirFiles, Comparator.comparing(File::getName).reversed());
SequencedFileNameMatcher matcher = new SequencedFileNameMatcher(nameNoExt, ext);
int lastFileNumber = 0;
int lastFileNumberBeforeTheLastGap = 0;
int previousNumber = 0;
for (int i = 0; i < allDirFiles.length; i++)
{
File file = allDirFiles[i];
if (!file.isFile())
{
continue;
}
int sequenceNumber = matcher.findSequenceNumber(file.getName());
if (sequenceNumber == 0)
{
continue;
}
if (lastFileNumber == 0)
{
lastFileNumber = sequenceNumber;
}
if (sequenceNumber < previousNumber - 1)
{
lastFileNumberBeforeTheLastGap = sequenceNumber;
break;
}
previousNumber = sequenceNumber;
}
return lastFileNumberBeforeTheLastGap != 0 ? lastFileNumberBeforeTheLastGap : lastFileNumber;
}
/**
* Updates the field {@link #logFile} for resolved path to the log File. Closes the old log file, does
* cleanup of old rotation log files based on {@link #numLogFiles}, and opens the new file
* adding the log header lines.
*
* @param newLogFile
* The new log file to be used.
*/
private void switchLogFile(File newLogFile)
{
if (this.logFile != null && this.logFile.equals(newLogFile))
{
return;
}
close(false, false);
this.logFile = newLogFile;
LOG.log(Level.CONFIG, "Log file name set to %s.", logFile);
if (this.logFile == null)
{
return;
}
boolean isRotationEnabled = isRotationEnabled();
if (client != null)
{
if (!session.isRunning())
{
return;
}
client.open(newLogFile.getPath());
if (isRotationEnabled)
{
client.cleanupRotation(logFile.getPath(), numLogFiles);
}
return;
}
try
{
this.logWriter = new FileWriter(logFile, true);
}
catch (IOException e)
{
LOG.log(Level.WARNING, e, "Not able to open log file %s.", logFile.getAbsolutePath());
}
if (isRotationEnabled)
{
cleanupRotation(logFile, numLogFiles);
}
}
/** Writes the system messages when a new file name is set. */
private void writeSystemHeader()
{
if ((client != null || logFile != null) && loggingLevel > LoggingLevel.OFF.toInt())
{
if (isAppserver)
{
writeMessage("Agent Starting Up -- " + Version.getFWDVersion(),
LoggingEntryType.ASPlumbing,
LoggingExecEnv.AS_AUX,
LoggingSubsys.MSAS, // todo: classic?
LoggingLevel.ERROR);
}
writeMessage("Logging level set to = " + loggingLevel, LoggingSubsys.EMPTY.toString());
writeMessage(activatedTypeLevelPairs.size() == 0 ?
"No entry types are activated" :
"Log entry types activated: " + getLogEntryTypes().toStringMessage(),
LoggingSubsys.EMPTY.toString());
}
}
/**
* Assigns the log file size threshold in bytes for rolling over to a new file.
*
* Valid values are either 0 for no limit (limited only by the system),
* or in the range 500_000 - 2_147_483_647.
*
* Can be set only command line on initialization.
*
* @param fileSize
* The log file size threshold for rolling over to a new file.
*/
private boolean setLogThreshold(int fileSize)
{
if (fileSize == 0)
{
this.logThreshold = fileSize;
return true;
}
if (fileSize < MIN_LOG_FILE_THRESHOLD_BYTES)
{
recordOrThrowError(11065, "-logthreshold must be set to a value between 500000 and 2147483647.",
false);
return false;
}
this.logThreshold = fileSize;
if (this.logWriter != null)
{
// only after initialization
writeMessage("Log file threshold set to = " + this.logThreshold);
}
return true;
}
/**
* Checks if file rotation is enabled. If {@link #logThreshold} has been initialized (having a value
* greater than 0), log file rotation is enabled.
*
* @return <code>true</code> if rotation is enabled, <code>false</code> otherwise.
*/
private boolean isRotationEnabled()
{
return logThreshold > 0;
}
/**
* Sets the number of log files that will be stored in total
* (including both backed up log files and the current log).
* Can be set only command line.
*
* @param numLogFiles
* The number of log files in total.
*/
private boolean setNumLogFiles(int numLogFiles)
{
if (numLogFiles == 1)
{
recordOrThrowError(11067, "-numlogfiles cannot be set to 1", false);
return false;
}
if (numLogFiles > MAX_NUM_LOG_FILES)
{
recordOrThrowError( 14416, "-numlogfiles cannot be set to a value greater than 999999", false);
return false;
}
this.numLogFiles = numLogFiles;
if (this.logWriter != null)
{
// only after initialization
writeMessage("The number of log files in total is set to = " + this.numLogFiles);
}
return true;
}
/**
* Assigns the file path to which log entries and stack traces are written.
*
* @param isProcedureCall
* Flag to identify the method is called by the execution of a converted 4GL procedure.
* @param logFileConfig
* The log file path.
*/
private synchronized void setLogFileName(boolean isProcedureCall, String logFileConfig)
{
if (isInitialized.get() && isAppserver)
{
recordOrShowWarning(4052, "LOGFILE-NAME is not a setable attribute for PSEUDO-WIDGET.", true);
return;
}
if (!hasContent(logFileConfig))
{
switchLogFile(null);
resetEnabled();
return;
}
logFileConfig = logFileConfig.trim();
if (client != null)
{
if (!client.isWritable(logFileConfig))
{
recordOrShowWarning(11076, "Cannot open log file " + logFileConfig + ", errno 3", false);
recordOrThrowError(4487, "Unable to open file for PSEUDO-WIDGET.", true);
return;
}
String newFilePath =
isRotationEnabled() ? client.makeSequencedLogFile(logFileConfig, logThreshold) : logFileConfig;
switchLogFile(new File(newFilePath));
}
else
{
File newLogFile = new File(logFileConfig);
if (!isWritable(newLogFile))
{
recordOrShowWarning(11076, "Cannot open log file " + logFileConfig + ", errno 3", false);
recordOrThrowError(4487, "Unable to open file for PSEUDO-WIDGET.", true);
return;
}
File newFile = isRotationEnabled() ? makeSequencedLogFile(logFileConfig, logThreshold) : newLogFile;
switchLogFile(newFile);
}
resetEnabled();
if (isProcedureCall)
{
writeSystemHeader();
}
}
/**
* Formats the parameters to the 4GL log record format.
*
* @param logLvl
* The logging level. See {@link LoggingLevel}.
* @param subsys
* The subsystem name.
* @param msg
* The message.
* @param threadId
* The client thread id or server thread id for AppServer and WebSpeed.
* @param date
* The date of the log record.
* @param execEnv
* The execution environment.
*
* @return The formatted string line representing the log record.
*/
private String createLogLine(int logLvl,
String subsys,
String msg,
long threadId,
Date date,
String execEnv)
{
String formattedDateTime = DATETIME_FORMATTER.format(date);
String formattedSubsys = LoggingSubsys.EMPTY.toString().equals(subsys) ? subsys : formatSubsys(subsys);
String formatProcessId = formatSystemId(clientProcessId);
String formatThreadId = formatSystemId(threadId);
return "[" + formattedDateTime + "] P-" + formatProcessId + " T-" + formatThreadId + " " + logLvl + " " +
execEnv + " " + formattedSubsys + " " + msg + System.lineSeparator();
}
/**
* Writes the log record to the log file server-side.
*
* @param logRecord
* The record to be written to the log file.
*
* @return <code>true</code> if the record has been written successfully, <code>false</code> otherwise.
*/
private boolean writeLocal(String logRecord)
{
try
{
Optional<Future<Boolean>> writeFutureOptional = writeExecutor.write(clientProcessId,
logFile.getAbsolutePath(),
isRotationEnabled(),
logWriter,
logRecord);
if (writeFutureOptional.isPresent())
{
return writeFutureOptional.get().get();
}
return true;
}
catch (InterruptedException e)
{
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "Interrupted while writing log [%s] to file %s.", logRecord, logFile);
}
return false;
}
catch (Exception e)
{
LOG.log(Level.WARNING, e, "Could't write log [%s] to file %s.", logRecord, logFile);
return false;
}
}
/**
* Writes the log record to the log file client-side.
*
* @param logRecord
* The record to be written to the log file.
* @param isAutoLogging
* A flag to indicate if auto logging from a subsystem or a user call from converted procedure.
*
* @return <code>true</code> if the record has been written successfully, <code>false</code> otherwise.
*/
private boolean writeRemote(String logRecord, boolean isAutoLogging)
{
if (!session.isRunning())
{
return true;
}
if (isAutoLogging)
{
LogicalTerminal.getInstance().addLogRecord(logRecord);
return true;
}
boolean successful = client.write(logRecord);
if (successful)
{
remoteFileSize.addAndGet(logRecord.getBytes().length);
}
return successful;
}
/**
* Format the subsystem string. It should take up a space defined by
* {@link #SUBSYS_CHARS_COUNT}.
*
* @param subsys
* Subsystem name.
*
* @return The formatted string for subsystem.
*/
private String formatSubsys(String subsys)
{
if (subsys == null || subsys.isEmpty())
{
subsys = LoggingSubsys.DEFAULT.toString();
}
if (subsys.equals(LoggingSubsys.EMPTY.toString()) ||
subsys.equals(LoggingSubsys.AS.toString()) ||
subsys.equals(LoggingSubsys.MSAS.toString()))
{
return subsys;
}
if (subsys.length() > SUBSYS_CHARS_COUNT)
{
subsys = subsys.substring(0, SUBSYS_CHARS_COUNT);
return subsys;
}
int whitespaceCount = SUBSYS_CHARS_COUNT - subsys.length();
String whitespaces = new String(new char[whitespaceCount]).replace("\0", " ");
return subsys + whitespaces;
}
/**
* Creates the next file in the sequence.
*/
private void rotateLogFile()
{
try
{
SequencedFileName sequencedFileName = new SequencedFileName(logFile.getName());
if (!sequencedFileName.isValid)
{
return;
}
String nextFileName = formatFileName(sequencedFileName,
nextSequenceNumber(sequencedFileName.sequenceNumberInt));
File newLogFile = new File(logFile.getParentFile(), nextFileName);
switchLogFile(newLogFile);
}
catch (Exception e)
{
LOG.log(Level.WARNING, "Couldn't rotate log file " + logFile.getPath());
}
}
/**
* Raise an error condition and optionally record the given error.
*
* @param num
* The error number.
* @param text
* The text of the error message.
* @param prefix
* <code>true</code> to prepend a double asterisk prefix to the
* formatted error message, <code>false</code> to omit this text.
*
* @throws ErrorConditionException
*/
private void recordOrThrowError(int num, String text, boolean prefix)
{
ErrorManager.recordOrThrowLogManagerError(num, text, prefix);
}
/**
* Manufacture a warning text based on the given parameters.
*
* @param num
* The error number.
* @param text
* The text of the error message.
* @param prefix
* <code>true</code> to prepend a double asterisk prefix to the formatted error
* message, <code>false</code> to omit this text.
*/
private void recordOrShowWarning(int num, String text, boolean prefix)
{
ErrorManager.recordOrShowLogManagerWarning(num, text, prefix);
}
/**
* Interface to unite all implementations in returning common file name attributes - file name prefix
* (clean name without number sequence or extension) and extension.
*/
private interface FileName {
/**
* Returns the file name without extension.
*
* @return The file name without extension.
*/
String getPrefix();
/**
* Returns the file extension.
*
* @return The file extension.
*/
String getExt();
}
/** A class for generic file names. */
private static class GenericFileName
implements FileName
{
/** The name before the extension. */
private String nameNoExt;
/** The file extension, might not be present. */
private String ext;
/** Package private constructor to create an instance of GenericFileName. */
GenericFileName(String fileName)
{
if (!fileName.contains("."))
{
nameNoExt = fileName;
ext = null;
return;
}
int lastDotIndex = fileName.lastIndexOf(".");
nameNoExt = fileName.substring(0, lastDotIndex);
ext = fileName.substring(lastDotIndex + 1);
ext = ext.trim().isEmpty() ? null : ext.trim();
}
/**
* Returns the file name without extension.
*
* @return The file name without extension.
*/
public String getPrefix()
{
return nameNoExt;
}
/**
* Returns the file extension.
*
* @return The file extension.
*/
public String getExt()
{
return ext;
}
}
/** A parser for log file names including a sequence number for rotation. */
private static class SequencedFileName
implements FileName
{
/** A pattern to match the file name (prefix), 6-digit sequence number and optional file extension. */
private static final Pattern PATTERN = Pattern.compile("^(.*)\\.(\\d{6})(\\.([^.]+))?$");
/** <code>true</code> if the file name matches the generic pattern, <code>false</code> otherwise. */
private boolean isValid;
/** The name prefix before the sequence number. */
private String nameNoSequenceNoExt;
/** The file extension, might not be present. */
private String ext;
/** The sequence number. */
private int sequenceNumberInt;
/** Package private constructor to create an instance of SequencedFileName. */
SequencedFileName(String fileName)
{
Matcher sequencedNameMatcher = PATTERN.matcher(fileName);
isValid = sequencedNameMatcher.matches();
if (!isValid)
{
return;
}
nameNoSequenceNoExt = sequencedNameMatcher.group(1);
String currentSequenceNumber = sequencedNameMatcher.group(2);
ext = sequencedNameMatcher.group(4);
sequenceNumberInt = Integer.parseInt(currentSequenceNumber);
}
/**
* Returns the file name without sequence number and extension.
*
* @return The file name without extension.
*/
public String getPrefix()
{
return nameNoSequenceNoExt;
}
/**
* Returns the file extension.
*
* @return The file extension.
*/
public String getExt()
{
return ext;
}
}
/** A matcher for log file names including certain prefix, extension and sequence number for rotation. */
private static class SequencedFileNameMatcher
{
/** The name prefix before the sequence number. */
String nameNoSequenceNoExt;
/** The file extension, might not be present. */
String ext;
/** Pattern to match the sequence number in the log file name. */
Pattern sequencedNamePattern;
/**
* Package private constructor to create an instance of SequencedFileNameMatcher.
*
* @param file
* The file.
*/
SequencedFileNameMatcher(File file)
{
SequencedFileName sequencedFileName = new SequencedFileName(file.getName());
initialize(sequencedFileName.getPrefix(), sequencedFileName.getExt());
}
/**
* Package private constructor to create an instance of SequencedFileNameMatcher.
* @param nameNoSequenceNoExt
* The file name without number sequence and extension.
* @param ext
* The file extension.
*/
SequencedFileNameMatcher(String nameNoSequenceNoExt, String ext)
{
initialize(nameNoSequenceNoExt, ext);
}
/**
* Returns if the file name matches the pattern of the matcher.
*
* @param fileName
* The file name to match against the pattern.
*
* @return <code>true</code> if the file name matches the pattern of the matcher,
* <code>false</code> otherwise.
*/
boolean matches(String fileName)
{
Matcher matcher = sequencedNamePattern.matcher(fileName);
return matcher.matches();
}
/**
* Finds the sequence number in the log file name that matches the pattern.
*
* @param fileName
* The file name to match against the pattern.
*
* @return The sequence number. <code>0</code> if no match found.
*/
int findSequenceNumber(String fileName)
{
Matcher matcher = sequencedNamePattern.matcher(fileName);
if (!matcher.matches())
{
return 0;
}
return Integer.parseInt(matcher.group(1));
}
/**
* Returns the file name without the number sequence, keeping the base name (prefix) and ext if
* available.
*
* @return The file name without the number sequence.
*/
String getNoSequenceName()
{
return ext == null ? nameNoSequenceNoExt : nameNoSequenceNoExt + "." + ext;
}
/**
* Common logic for all constructors.
*
* @param nameNoSequenceNoExt
* The file name without number sequence and extension.
* @param ext
* The file extension.
*/
private void initialize(String nameNoSequenceNoExt, String ext)
{
this.nameNoSequenceNoExt = nameNoSequenceNoExt;
this.ext = ext;
String sequencePatternString = ext == null ?
nameNoSequenceNoExt + "\\.(\\d{6})" :
nameNoSequenceNoExt + "\\.(\\d{6})\\." + ext;
this.sequencedNamePattern = Pattern.compile(sequencePatternString);
}
}
}