Here's a fun fact to get the ball rolling: the constructor for any Exception
class will generate the current stack trace (no need to throw). So you can, for instance, get the stack trace in your logger constructor:
final String firstLine;
public Logger()
{
String stackTrace = new DmlException()
.getStackTraceString().substringAfter('\n');
// being in the constructor adds one line at the top of the stack
// ignore the first line to get the stack trace of the calling context
firstLine = stackTrace.substringBefore('\n');
}
That provides a good basis to start working the problem, but we still need to understand what sort of input/output combos we might need to support. To that end, I spun up a quick class with some basic types of calling context (first line of stack trace in comments):
public class Demo
{
public Logger l;
public Demo()
{ // top-level constructor
l = new Logger(); //Class.Demo.<init>: line 6, column 1
}
public static Logger foo()
{ // top-level method
return new Logger(); //Class.Demo.foo: line 10, column 1
}
public class Sub
{
Logger l;
public Sub()
{ // inner-class constructor
l = new Logger(); //Class.Demo.Sub.<init>: line 17, column 1
}
public Logger bar()
{ // inner-class method
return new Logger(); //Class.Demo.Sub.bar: line 21, column 1
}
}
public static String someProperty
{
get
{ // property getter
system.debug(new Logger()); //Class.Demo.__sfdc_someProperty: line 28, column 1
return someProperty;
}
set
{ // property setter
system.debug(new Logger()); //Class.Demo.__sfdc_someProperty: line 33, column 1
someProperty = value;
}
}
}
It's also a bit different from anonymous scripts:
Logger l = new Logger(); //AnonymousBlock: line 1, column 1
Logger foo()
{ // anonymous method
return new Logger(); //AnonymousBlock: line 4, column 1
}
class Anon
{
Logger l;
Anon()
{ // anonymous class constructor
l = new Logger(); //Class.Anon.<init>: line 10, column 1
}
Logger bar()
{ // anonymous class method
return new Logger(); //Class.Anon.bar: line 15, column 1
}
}
Okay, so a Regular Expression seems like the way to go. Let's try one out:
Explanation
/
^(?:class\.)?([^.]+)\.?([^\.\:]+)?[\.\:]?([^\.\:]*): line (\d+), column (\d+)$
/ gim
^
asserts position at start of a line
- Non-capturing group
(?:class\.)?
?
Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
class
matches the characters class
literally (case insensitive)
\.
matches the character .
literally (case insensitive)
- 1st Capturing Group
([^.]+)
- Match a single character not present in the list below
[^.]+
+
Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
.
matches the character .
literally (case insensitive)
\.?
matches the character .
literally (case insensitive)
?
Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
- 2nd Capturing Group
([^\.\:]+)?
?
Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
- Match a single character not present in the list below
[^\.\:]+
+
Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
\.
matches the character .
literally (case insensitive)
\:
matches the character :
literally (case insensitive)
- Match a single character present in the list below [.:]?
?
Quantifier — Matches between zero and one times, as many times as possible, giving back as needed (greedy)
\.
matches the character .
literally (case insensitive)
\:
matches the character :
literally (case insensitive)
- 3rd Capturing Group
([^\.\:]*)
- Match a single character not present in the list below
[^\.\:]*
*
Quantifier — Matches between zero and unlimited times, as many times as possible, giving back as needed (greedy)
\.
matches the character .
literally (case insensitive)
\:
matches the character :
literally (case insensitive)
: line
matches the characters : line
literally (case insensitive)
- 4th Capturing Group
(\d+)
\d+
matches a digit (equal to [0-9]
)
+
Quantifier — Matches between one and unlimited times, as many times as possible, giving back as needed (greedy)
, column
matches the characters , column
literally (case insensitive)
- 5th Capturing Group
(\d+)
$
asserts position at the end of a line
In plain english:
- The entire search is case-insensitive.
- The line may or may not start with the substring
'class.'
. Ignore this substring.
- First Capturing Group (
group(1)
)
- If there are one or more
'.'
characters, match the remaining characters before the first '.'
character.
- If there is no
'.'
character, match the remaining characters before the ':'
character.
- Match the first
'.'
character if one is present. Ignore this substring.
- Second Capturing Group (
group(2)
)
- If there is no
'.'
character, match nothing.
- If there is one
'.'
character, match everything between it and the ':'
character.
- If there are two
'.'
characters, match everything between the first and second '.'
character.
- Match the second
'.'
character if one is present. Ignore this substring.
- Third Capturing Group (
group(3)
)
- If there are fewer than two
'.'
characters, match nothing.
- If there are two
'.'
characters, match everything between the second '.'
character and the ':'
character.
- Match
': column '
. This substring must be present. Ignore it.
- Fourth Capturing Group (
group(4)
)
- Match all digits. There must be at least one.
- Match
', line '
. This substring must be present. Ignore it.
- Fifth Capturing Group (
group(5)
)
- Match all digits. There must be at least one.
With that expression figured out, here's a class implementation that works for me in the scenarios I have tested:
public class Logger
{
public static Matcher generateMatcher(String firstLine)
{
return Pattern.compile(
'(?i)^(?:class\\.)?([^.]+)\\.?([^\\.\\:]+)?[\\.\\:]?([^\\.\\:]*): line (\\d+), column (\\d+)$'
).matcher(firstLine);
}
public final String className, methodName;
public final Integer line, column;
public Logger()
{
Matcher m = generateMatcher(
new DmlException()
.getStackTraceString()
.substringAfter('\n')
.substringBefore('\n')
);
if (m.find())
{
if (String.isBlank(m.group(3)))
{
className = m.group(1);
methodName = prettifyMethodName(m.group(2));
}
else
{
className = m.group(1) + '.' + m.group(2);
methodName = prettifyMethodName(m.group(3));
}
line = Integer.valueOf(m.group(4));
column = Integer.valueOf(m.group(5));
}
}
String prettifyMethodName(String name)
{
return (name == null) ? null :
name.replace('<init>', '(constructor) ')
.replace('__sfdc_', '(getter/setter) ');
}
}
The prettification is obviously an unnecessary step. For instance, with it removed, I get the following yields:
public class Demo
{
public Logger l;
public Demo()
{ // top-level class constructor
l = new Logger(); // Logger:[className=Demo, column=1, line=6, methodName=<init>]
}
public static Logger foo()
{ // top-level class method
return new Logger(); // Logger:[className=Demo, column=1, line=10, methodName=foo]
}
public class Sub
{
public Logger l;
public Sub()
{ // inner class constructor
l = new Logger(); // Logger:[className=Demo.Sub, column=1, line=17, methodName=<init>]
}
public Logger bar()
{ // inner class method
return new Logger(); // Logger:[className=Demo.Sub, column=1, line=21, methodName=bar]
}
}
public static String someProperty
{
get
{ // property getter
system.debug(new Logger()); // Logger:[className=Demo, column=1, line=33, methodName=__sfdc_someProperty]
return someProperty;
}
set
{ // property setter
system.debug(new Logger()); // Logger:[className=Demo, column=1, line=28, methodName=__sfdc_someProperty]
someProperty = value;
}
}
}