PatternCompiler.java
package io.jstach.rainbowgum.pattern.format;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProvider;
import io.jstach.rainbowgum.ServiceRegistry;
import io.jstach.rainbowgum.format.AnsiSupport;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.CompositeFactory;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.KeywordFactory;
import io.jstach.rainbowgum.pattern.internal.Node;
import io.jstach.rainbowgum.pattern.internal.Node.CompositeNode;
import io.jstach.rainbowgum.pattern.internal.Node.End;
import io.jstach.rainbowgum.pattern.internal.Node.FormattingNode;
import io.jstach.rainbowgum.pattern.internal.Node.KeywordNode;
import io.jstach.rainbowgum.pattern.internal.Node.LiteralNode;
import io.jstach.rainbowgum.pattern.internal.Parser;
import io.jstach.rainbowgum.pattern.internal.ScanException;
/**
* Compiles a pattern into a formatter.
*/
public sealed interface PatternCompiler {
/**
* Compiles a pattern into a formatter.
* @param pattern logback style pattern.
* @return formatter.
*/
public LogFormatter compile(String pattern);
/**
* Creates a pattern compiler builder. If nothing is set the default pattern registry
* and formatting config will be used.
* @return builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Creates a pattern compiler provider from a builder lambda.
* @param consumer builder will be provided to the consumer.
* @return provider of pattern compiler.
*/
public static LogProvider<PatternCompiler> of(Consumer<Builder> consumer) {
return (name, config) -> {
var services = config.serviceRegistry();
var registry = findService(services, PatternRegistry.class, name, LogProperties.DEFAULT_NAME)
.orElseGet(() -> PatternRegistry.of());
var patternConfig = findService(services, PatternConfig.class, name, LogProperties.DEFAULT_NAME)
.orElseGet(() -> {
boolean ansiDisable = config.properties()
.forKey(LogProperties.GLOBAL_ANSI_DISABLE_PROPERTY) //
.ofBoolean() //
.or(() -> !AnsiSupport.isAnsiSupported())
.validateNow(PatternCompiler.class);
var b = PatternConfig.builder(name)
.propertyFunction(PatternConfig.propertyFunction(config.properties(),
PatternConfig.PATTERN_PROPERY_PREFIX))
.fromProperties(config.properties());
if (ansiDisable) {
b.ansiDisabled(true);
}
return b.build();
});
Builder b = builder();
b.patternRegistry(registry);
b.patternConfig(patternConfig);
consumer.accept(b);
return b.build();
};
}
private static <T> Optional<T> findService(ServiceRegistry services, Class<T> c, String... names) {
Stream<String> sn = Stream.of(names);
return sn.flatMap(s -> Stream.ofNullable(services.findOrNull(c, s))).findFirst();
}
/**
* Builder for {@link PatternCompiler}.
*/
public final static class Builder {
private @Nullable PatternRegistry patternRegistry;
private @Nullable PatternConfig patternConfig;
private Builder() {
}
/**
* Pattern keyword registry.
* @param patternRegistry keyword names registry.
* @return this.
*/
public Builder patternRegistry(PatternRegistry patternRegistry) {
this.patternRegistry = patternRegistry;
return this;
}
/**
* Formatting config that has platform config like time zone etc.
* @param patternConfig config for formatting of keywords.
* @return this.
*/
public Builder patternConfig(PatternConfig patternConfig) {
this.patternConfig = patternConfig;
return this;
}
/**
* Creates a pattern compiler.
* @return pattern compiler.
*/
public PatternCompiler build() {
var patternRegistry_ = patternRegistry;
var formatterConfig_ = patternConfig;
if (patternRegistry_ == null) {
patternRegistry_ = PatternRegistry.of();
}
if (formatterConfig_ == null) {
formatterConfig_ = PatternConfig.of();
}
return new Compiler(patternRegistry_, formatterConfig_);
}
}
}
final class Compiler implements PatternCompiler {
private final PatternConfig config;
private final PatternRegistry registry;
Compiler(PatternRegistry registry, PatternConfig config) {
this.config = config;
this.registry = registry;
}
@Override
public LogFormatter compile(String pattern) {
return compile(pattern, true);
}
/*
* autoAppendException exists as a seam for tests that want to check what a single
* keyword compiles to in isolation, without the default exception formatter that
* would otherwise always be appended (see below). Real callers only ever go through
* the one-arg compile(String) above.
*/
LogFormatter compile(String pattern, boolean autoAppendException) {
try {
Parser p = new Parser(pattern);
Node parsed = p.parse();
LogFormatter formatter = compile(parsed);
if (autoAppendException && !containsExceptionKeyword(parsed)) {
formatter = LogFormatter.builder().add(formatter).throwable().build();
}
return formatter;
}
catch (ScanException | IllegalStateException e) {
throw new IllegalArgumentException("Pattern is invalid: " + pattern, e);
}
}
LogFormatter compile(Node start) {
var b = LogFormatter.builder();
if (start == Node.end()) {
return b.build();
}
for (Node n = start; n != Node.end();) {
n = switch (n) {
case End e -> {
yield e;
}
case LiteralNode ln -> {
b.text(ln.value());
yield ln.next();
}
case FormattingNode fn -> {
PatternFormatterFactory f = registry.getOrNull(fn.keyword());
if (f == null) {
// TODO better error message
throw new IllegalStateException("Missing formatter for key: " + fn.keyword());
}
var _child = switch (fn) {
case CompositeNode cn -> cn.childNode();
case KeywordNode kn -> null;
};
var formatter = switch (f) {
case KeywordFactory kf -> kf.create(config, fn);
case CompositeFactory cf -> {
LogFormatter child = _child == null ? null : compile(_child);
yield cf.create(config, fn, child);
}
};
b.add(formatter);
yield fn.next();
}
};
}
return b.build();
}
/*
* A separate, non-mutating pass (rather than tracking state during compile() itself)
* so that compile(Node) keeps its original, simple recursive shape. Needs its own
* recursion into composite children for the same reason compile(Node) does - a
* keyword like %red(%ex) nests the exception keyword inside a child pattern.
*
* Checked via the resolved factory's isExceptionFormatter() rather than a fixed set
* of known keyword strings, so custom-registered keywords (e.g. Spring Boot's
* %wEx/%wex, registered entirely outside this module) are recognized too.
*/
private boolean containsExceptionKeyword(Node start) {
for (Node n = start; n != Node.end();) {
switch (n) {
case End e -> {
return false;
}
case LiteralNode ln -> n = ln.next();
case FormattingNode fn -> {
PatternFormatterFactory f = registry.getOrNull(fn.keyword());
if (f != null && f.isExceptionFormatter()) {
return true;
}
var child = switch (fn) {
case CompositeNode cn -> cn.childNode();
case KeywordNode kn -> null;
};
if (child != null && containsExceptionKeyword(child)) {
return true;
}
n = fn.next();
}
}
}
return false;
}
}