PatternConfigurator.java

package io.jstach.rainbowgum.pattern.format;

import java.nio.charset.Charset;
import java.time.Instant;
import java.time.ZoneId;
import java.util.function.Function;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogConfig;
import io.jstach.rainbowgum.LogEncoder;
import io.jstach.rainbowgum.LogOutput.OutputType;
import io.jstach.rainbowgum.LogProperties;
import io.jstach.rainbowgum.LogProvider;
import io.jstach.rainbowgum.LogProviderRef;
import io.jstach.rainbowgum.ServiceRegistry;
import io.jstach.rainbowgum.annotation.LogConfigurable;
import io.jstach.rainbowgum.annotation.LogConfigurable.ConvertParameter;
import io.jstach.rainbowgum.annotation.LogConfigurable.KeyParameter;
import io.jstach.rainbowgum.annotation.LogConfigurable.PassThroughParameter;
import io.jstach.rainbowgum.pattern.format.DefaultFormatterConfig.StandardPropertyFunction;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider;
import io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator;
import io.jstach.svc.ServiceProvider;

/**
 * Configures Logback style pattern encoders and offers provision with the URI scheme
 * {@value PatternEncoder#PATTERN_SCHEME}.
 *
 * @see PatternEncoderBuilder
 */
@ServiceProvider(RainbowGumServiceProvider.class)
public final class PatternConfigurator implements Configurator {

	/**
	 * Property that will disable this configurator from replacing the default encoder
	 * with the pattern encoder. The default encoder is normally the TTLL encoder but if
	 * this configurator is loaded the pattern encoder will be used and if a no pattern is
	 * provided a default one is picked.
	 */
	public static final String LOGGING_PATTERN_DISABLE_PROPERTY = LogProperties.ROOT_PREFIX + "pattern.disable";

	/**
	 * For service loader to call.
	 */
	public PatternConfigurator() {
		// for service loader.
	}

	@Override
	public int priority() {
		return -1 << 2; // internal group 2
	}

	static final String DEFAULT_PATTERN = """
			%cyan(%date{HH:mm:ss.SSS}) %clr([%thread]){faint} %highlight(%-5level){ERROR=red bold, INFO=blue bold, WARN=red bold} %magenta(%logger{36}) - %msg%n%ex""";

	@Override
	public boolean configure(LogConfig config, Pass pass) {
		var services = config.serviceRegistry();
		String n = ServiceRegistry.DEFAULT_SERVICE_NAME;
		services.putIfAbsent(PatternRegistry.class, n, PatternRegistry::of);
		services.putIfAbsent(PatternCompiler.class, n, () -> PatternCompiler.of(b -> {
		}).provide(n, config));
		config.encoderRegistry().register(PatternEncoder.PATTERN_SCHEME, new PatternEncoderProvider());
		var disable = config.properties()
			.forKey(LOGGING_PATTERN_DISABLE_PROPERTY)
			.ofBoolean()
			.or(false)
			.validateNow(PatternConfigurator.class);
		if (!disable) {
			config.encoderRegistry().setEncoderForOutputType(OutputType.CONSOLE_OUT, (name, c) -> {
				PatternEncoderBuilder b = new PatternEncoderBuilder(name);
				b.pattern(DEFAULT_PATTERN);
				b.fromProperties(c.properties());
				return b.build().provide(name, config);
			});
		}
		return true;
	}

	@LogConfigurable(name = "PatternEncoderBuilder", prefix = LogProperties.ENCODER_PREFIX + "pattern.")
	static LogProvider<LogEncoder> provideEncoder(@KeyParameter String name, String pattern,
			@PassThroughParameter @Nullable PatternCompiler patternCompiler,
			@ConvertParameter("convertCharset") @Nullable Charset charset, @Nullable Integer maxBufferSize) {
		return (n, config) -> {
			var compiler = patternCompiler;
			if (compiler == null) {
				compiler = PatternCompiler.of(b -> {
				}).provide(name, config);
			}
			var formatter = compiler.compile(pattern);
			var builder = LogEncoder.builder(formatter);
			builder.description(pattern);
			if (charset != null) {
				builder.charset(charset);
			}
			if (maxBufferSize != null) {
				builder.maxBufferSize(maxBufferSize);
			}
			return builder.build().provide(n, config);
		};
	}

	static Charset convertCharset(String charset) {
		return Charset.forName(charset);
	}

	@LogConfigurable(name = "PatternConfigBuilder", prefix = PatternConfig.PATTERN_CONFIG_PREFIX)
	static PatternConfig provideFormatterConfig(@KeyParameter String name, //
			@ConvertParameter("convertZoneId") @Nullable ZoneId zoneId, //
			@Nullable String lineSeparator, //
			@Nullable Boolean ansiDisabled, //
			@PassThroughParameter @Nullable Function<String, @Nullable String> propertyFunction, //
			@PassThroughParameter @Nullable Instant startTime, //
			@ConvertParameter("convertSequenceNumberStart") @Nullable Long sequenceNumberStart, //
			@ConvertParameter("convertAbbreviatorCache") PatternConfig.@Nullable CacheType abbreviatorCache) {
		PatternConfig dc = PatternConfig.of();
		ansiDisabled = ansiDisabled == null ? dc.ansiDisabled() : ansiDisabled;
		lineSeparator = lineSeparator == null ? dc.lineSeparator() : lineSeparator;
		zoneId = zoneId == null ? dc.zoneId() : zoneId;
		propertyFunction = propertyFunction == null ? StandardPropertyFunction.INSTANCE : propertyFunction;
		startTime = startTime == null ? Instant.now() : startTime;
		long sequenceNumberStart_ = sequenceNumberStart == null ? dc.sequenceNumberStart() : sequenceNumberStart;
		abbreviatorCache = abbreviatorCache == null ? dc.abbreviatorCache() : abbreviatorCache;
		return new SimpleFormatterConfig(zoneId, lineSeparator, ansiDisabled, propertyFunction, startTime,
				sequenceNumberStart_, abbreviatorCache);

	}

	static Long convertSequenceNumberStart(String s) {
		return Long.valueOf(s);
	}

	static PatternConfig.CacheType convertAbbreviatorCache(String s) {
		return PatternConfig.CacheType.parse(s);
	}

	static ZoneId convertZoneId(String zoneId) {
		return ZoneId.of(zoneId);
	}

}

record PatternEncoderProvider() implements LogEncoder.EncoderProvider {

	@Override
	public LogProvider<LogEncoder> provide(LogProviderRef ref) {
		return (name, config) -> {
			PatternEncoderBuilder b = new PatternEncoderBuilder(name);
			b.fromProperties(config.properties(), ref);
			return b.build().provide(name, config);
		};
	}

}