You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
13 KiB
362 lines
13 KiB
/** |
|
* Object of this class is meant to represent a single log message that |
|
* can have parameters, specified by "%<number>" definitions |
|
* (e.g. "Thing %1 conflicts with thing %2, so we will have to remove %3"). |
|
* Log message only has to prepare (break into parts) provided human-readable |
|
* string once and then will be able to quickly perform argument insertion |
|
* (for which several convenient `Arg*()` methods are provided). |
|
* The supposed way to use `LogMessage` is is in conjunction with |
|
* `LoggerAPI`'s `Auto()` method that takes `Definition` with pre-filled |
|
* message (`m`) and type (`t`), then: |
|
* 1. (first time only) Generates a new `LogMessage` from them; |
|
* 2. Returns `LogMessage` object that, whos arguments are supposed to be |
|
* filled with `Arg*()` methods; |
|
* 3. When the appropriate amount of `Arg*()` calls (by the number of |
|
* specified "%<number>" tags) was made - logs resulting message. |
|
* (4). If message takes no arguments - no `Arg*()` calls are necessary; |
|
* (5). If new `Auto()` call is made before previous message was provided |
|
* enough arguments - error will be logged and previous message |
|
* will be discarded (along with it's arguments). |
|
* For more information about using it - refer to the documentation. |
|
* Copyright 2021 Anton Tarasenko |
|
*------------------------------------------------------------------------------ |
|
* This file is part of Acedia. |
|
* |
|
* Acedia is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* Acedia 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 General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
*/ |
|
class LogMessage extends AcediaObject; |
|
|
|
// Flag to prevent `Initialize()` being called several times in a row |
|
var private bool isInitialized; |
|
// With what level was this message initialized? |
|
var private LoggerAPI.LogLevel myLevel; |
|
|
|
/** |
|
* We essentially break specified log string into parts |
|
* (replacing empty parts with `none`) like this: |
|
* "This %1 is %3, not %2" => |
|
* * "This " |
|
* * " is " |
|
* * ", not " |
|
* * `none` |
|
* Arguments always fit between these parts, so if there is `N` parts, |
|
* there will be `N - 1` arguments. We consider that we are done filling |
|
* arguments when amount of `Arg*()` calls reaches that number. |
|
* |
|
* For future localization purposes we do not assume that arguments are |
|
* specified from left to right in order: in our example, |
|
* if we make following calls: `.Arg("one").Arg("caged").Arg("free")`, |
|
* we will get the string: "This one is free, not caged". |
|
* To remember the order we keep a special array, in our case it would be |
|
* [1, 3, 2], except normalized to start from zero: [0, 2, 1]. |
|
*/ |
|
// Parts of the initial message, broken by "%<number> tags |
|
var private array<Text> logParts; |
|
// Defines order of arguments: |
|
// `i` -> argument at what number to use at insertion place `i`? |
|
// (`i` starting from zero instead of `1`). |
|
var private array<int> normalizedArguments; |
|
|
|
// Only default value is used for this variable: it remembers what |
|
// `LogMessage` currently stores "garbage": temporary `Text` object to create |
|
// a log message. Making an `Arg*()` call on any other `LogMessage` will cause |
|
// progress of `default.dirtyLogMessage` to be reset, thus enforcing that only |
|
// one `LogMessage` can be in the process of filling itself with arguments at |
|
// a time and, therefore, only one can be "dirty": contain temporary |
|
// `Text` objects. |
|
// This way using `LogMessage` would not lead to accumulating large |
|
// amounts of trash objects, since only one of them can "make a mess". |
|
var private LogMessage dirtyLogMessage; |
|
// Arguments, collected so far by the `Arg*()` calls |
|
var private array<BaseText> collectedArguments; |
|
|
|
protected function Finalizer() |
|
{ |
|
isInitialized = false; |
|
_.memory.FreeMany(logParts); |
|
_.memory.FreeMany(collectedArguments); |
|
logParts.length = 0; |
|
collectedArguments.length = 0; |
|
normalizedArguments.length = 0; |
|
} |
|
|
|
/** |
|
* Initialize new `LogMessage` object by a given definition. |
|
* Can only be done once. |
|
* |
|
* Correct functionality is guaranteed when arguments start from either |
|
* `0` or `1` and then increase in order, without gaps or repetitions. |
|
* `Initialize()` will attempt to correctly initialize `LogMessage` in case |
|
* these rules are broken, by making assumptions about user's intentions, |
|
* but behavior in that case should be considered undefined. |
|
* |
|
* @param logMessageDefinition Definition to take message parameter from. |
|
*/ |
|
public final function Initialize(LoggerAPI.Definition logMessageDefinition) |
|
{ |
|
local int nextArgument; |
|
local Parser parser; |
|
local MutableText nextLogPart, nextChunk; |
|
local BaseText.Character percentCharacter; |
|
local array<int> parsedArguments; |
|
if (isInitialized) { |
|
return; |
|
} |
|
isInitialized = true; |
|
myLevel = logMessageDefinition.l; |
|
percentCharacter = _.text.GetCharacter("%"); |
|
parser = _.text.ParseString(logMessageDefinition.m); |
|
nextLogPart = _.text.Empty(); |
|
// General idea is simply to repeat: parse until "%" -> parse "%<number>" |
|
while (!parser.HasFinished()) |
|
{ |
|
parser.MUntil(nextChunk, percentCharacter).Confirm(); |
|
nextLogPart.Append(nextChunk); |
|
// If we cannot parse "%" after `MUntil(nextChunk, percentCharacter)`, |
|
// then we have parsed everything |
|
if (!parser.Match(P("%")).Confirm()) { |
|
break; |
|
} |
|
// We need to check whether it i really "%<number>" tag and not |
|
// just a "%" symbol without number |
|
if (parser.MInteger(nextArgument).Confirm()) |
|
{ |
|
parsedArguments[parsedArguments.length] = nextArgument; |
|
logParts[logParts.length] = nextLogPart.Copy(); |
|
nextLogPart.Clear(); |
|
} |
|
else |
|
{ |
|
// If it is just a symbol - simply add it |
|
nextLogPart.AppendCharacter(percentCharacter); |
|
parser.R(); |
|
} |
|
} |
|
logParts[logParts.length] = nextLogPart.Copy(); |
|
parser.FreeSelf(); |
|
nextLogPart.FreeSelf(); |
|
CleanupEmptyLogParts(); |
|
NormalizeArguments(parsedArguments); |
|
} |
|
|
|
// Since `none` and empty `Text` will be treated the same way by the `Append()` |
|
// operation, we do not need to keep empty `Text` objects and can simply |
|
// replace them with `none`s |
|
private final function CleanupEmptyLogParts() |
|
{ |
|
local int i; |
|
for (i = 0; i < logParts.length; i += 1) |
|
{ |
|
if (logParts[i].IsEmpty()) |
|
{ |
|
logParts[i].FreeSelf(); |
|
logParts[i] = none; |
|
} |
|
} |
|
} |
|
|
|
// Normalize enumeration by replacing them with natural numbers sequence: |
|
// [0, 1, 2, ...] in the same order: |
|
// [2, 6, 3] -> [0, 2, 1] |
|
// [-2, 0, 4, -7] -> [1, 2, 3, 0] |
|
// [1, 1, 2, 1] -> [0, 1, 3, 2] |
|
private final function NormalizeArguments(array<int> argumentsOrder) |
|
{ |
|
local int i; |
|
local int nextArgument; |
|
local int lowestArgument, lowestArgumentIndex; |
|
normalizedArguments.length = 0; |
|
normalizedArguments.length = argumentsOrder.length; |
|
while (nextArgument < normalizedArguments.length) |
|
{ |
|
// Find next minimal index and record next natural number |
|
// (`nextArgument`) into it |
|
for (i = 0; i < argumentsOrder.length; i += 1) |
|
{ |
|
if (argumentsOrder[i] < lowestArgument || i == 0) |
|
{ |
|
lowestArgumentIndex = i; |
|
lowestArgument = argumentsOrder[i]; |
|
} |
|
} |
|
argumentsOrder[lowestArgumentIndex] = MaxInt; |
|
normalizedArguments[lowestArgumentIndex] = nextArgument; |
|
nextArgument += 1; |
|
} |
|
} |
|
|
|
/** |
|
* Fills next argument in caller `LogMessage` with given `Text` argument. |
|
* |
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
|
* all arguments leads to message being logged. |
|
* |
|
* @param argument This argument will be managed by the `LogMessage`: |
|
* you should not deallocate it by hand or rely on passed `Text` to not |
|
* be deallocated. This also means that you should not pass `Text` objects |
|
* returned by `P()`, `C()` or `F()` calls. |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage Arg(/*take*/ BaseText argument) |
|
{ |
|
if (IsArgumentListFull()) { |
|
return self; |
|
} |
|
// Do we need to clean old `LogMessage` from it's arguments first? |
|
if (default.dirtyLogMessage != none && default.dirtyLogMessage != self) { |
|
default.dirtyLogMessage.Reset(); |
|
} |
|
default.dirtyLogMessage = self; // `self` is dirty with arguments now |
|
collectedArguments[collectedArguments.length] = argument; |
|
TryLogging(); |
|
return self; |
|
} |
|
|
|
/** |
|
* Outputs a message at appropriate level, if all of its arguments were filled. |
|
*/ |
|
public final function TryLogging() |
|
{ |
|
local Text assembledMessage; |
|
if (IsArgumentListFull()) |
|
{ |
|
// Last argument - have to log what we have collected |
|
assembledMessage = Collect(); |
|
_.logger.LogAtLevel(assembledMessage, myLevel); |
|
assembledMessage.FreeSelf(); |
|
} |
|
} |
|
|
|
// Check whether we have enough arguments to completely make log message: |
|
// each argument goes in between two log parts, so there is |
|
// `logParts.length - 1` arguments total. |
|
private final function bool IsArgumentListFull() |
|
{ |
|
return collectedArguments.length >= logParts.length - 1; |
|
} |
|
|
|
/** |
|
* Fills next argument in caller `LogMessage` with given `int` argument. |
|
* |
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
|
* all arguments leads to message being logged. |
|
* |
|
* @param argument This value will be converted into `Text` and pasted |
|
* into the log message. |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage ArgInt(int argument) |
|
{ |
|
return Arg(_.text.FromInt(argument)); |
|
} |
|
|
|
/** |
|
* Fills next argument in caller `LogMessage` with given `float` argument. |
|
* |
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
|
* all arguments leads to message being logged. |
|
* |
|
* @param argument This value will be converted into `Text` and pasted |
|
* into the log message. |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage ArgFloat(float argument) |
|
{ |
|
return Arg(_.text.FromFloat(argument)); |
|
} |
|
|
|
/** |
|
* Fills next argument in caller `LogMessage` with given `bool` argument. |
|
* |
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
|
* all arguments leads to message being logged. |
|
* |
|
* @param argument This value will be converted into `Text` and pasted |
|
* into the log message. |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage ArgBool(bool argument) |
|
{ |
|
return Arg(_.text.FromBool(argument)); |
|
} |
|
|
|
/** |
|
* Fills next argument in caller `LogMessage` with given `class<Object>` |
|
* argument. |
|
* |
|
* When used on `LogMessage` returned from `LoggerAPI.Auto()` call - filling |
|
* all arguments leads to message being logged. |
|
* |
|
* @param argument This value will be converted into `Text` and pasted |
|
* into the log message. |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage ArgClass(class<Object> argument) |
|
{ |
|
return Arg(_.text.FromClass(argument)); |
|
} |
|
|
|
/** |
|
* Resets current progress of filling caller `LogMessage` with arguments, |
|
* deallocating already passed ones. |
|
* |
|
* @return Caller `LogMessage` to allow for method chaining. |
|
*/ |
|
public final function LogMessage Reset() |
|
{ |
|
_.memory.FreeMany(collectedArguments); |
|
collectedArguments.length = 0; |
|
return self; |
|
} |
|
|
|
/** |
|
* Returns `LogMessage`, assembled with it's arguments into the `Text`. |
|
* |
|
* If some arguments were not yet filled - they will treated as empty `Text` |
|
* values. |
|
* |
|
* This result will be reset if `Reset()` method is called or another |
|
* `LogMessage` starts filling itself with arguments. |
|
* |
|
* @return Caller `LogMessage`, assembled with it's arguments into the `Text`. |
|
*/ |
|
public final function Text Collect() |
|
{ |
|
local int i, argumentIndex; |
|
local Text result; |
|
local BaseText nextArgument; |
|
local MutableText builder; |
|
if (logParts.length == 0) { |
|
return P("").Copy(); |
|
} |
|
builder = _.text.Empty(); |
|
for (i = 0; i < logParts.length - 1; i += 1) |
|
{ |
|
nextArgument = none; |
|
// Since arguments might not be specified in order - |
|
argumentIndex = normalizedArguments[i]; |
|
if (argumentIndex < collectedArguments.length) { |
|
nextArgument = collectedArguments[argumentIndex]; |
|
} |
|
builder.Append(logParts[i]).Append(nextArgument); |
|
} |
|
builder.Append(logParts[logParts.length - 1]); |
|
result = builder.Copy(); |
|
builder.FreeSelf(); |
|
return result; |
|
} |
|
|
|
defaultproperties |
|
{ |
|
} |