PatternRegistry.java

package io.jstach.rainbowgum.pattern.format;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.annotation.CaseChanging;
import io.jstach.rainbowgum.pattern.format.DefaultPatternRegistry.CustomConverterKey;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.KeywordFactory;

/**
 * Pattern registry to register custom keywords.
 */
public sealed interface PatternRegistry {

	/*
	 * TODO this is a mutable component!
	 */

	/**
	 * Gets the formatter factory for the pattern key.
	 * @param key keyword.
	 * @return factory or null.
	 */
	public @Nullable PatternFormatterFactory getOrNull(String key);

	/**
	 * Registers a pattern key with a formatter factory. You may replace the built-in
	 * pattern formatters with {@link KeywordKey} or {@link ColorKey}s or create a new key
	 * with {@link PatternKey#of(String)}.
	 * @param key pattern keys.
	 * @param factory factory to create formatters from pattern keywords.
	 */
	public void register(PatternKey key, PatternFormatterFactory factory);

	/**
	 * Convenience to register factories that have keys associated with them by
	 * implementing {@link PatternKeyProvider}.
	 * @param <P> pattern formatter that has keys associated with it.
	 * @param factory factory that also has a key.
	 * @see #register(PatternKey, PatternFormatterFactory)
	 */
	default <P extends PatternKeyProvider & PatternFormatterFactory> void register(P factory) {
		register(factory.key(), factory);
	}

	/**
	 * Convenience function for {@link #register(PatternKey, PatternFormatterFactory)} as
	 * keyword factory is a lambda.
	 * @param key pattern keys.
	 * @param factory factory create formatter from a simple keyword.
	 */
	default void keyword(PatternKey key, KeywordFactory factory) {
		register(key, factory);
	}

	/**
	 * Creates a pattern registry with the defaults.
	 * @return default registry.
	 */
	public static PatternRegistry of() {
		return DefaultPatternRegistry.registerDefaults(new DefaultPatternRegistry());
	}

	/**
	 * Pattern keywords.
	 *
	 * @see KeywordKey
	 * @see ColorKey
	 */
	public sealed interface PatternKey {

		/**
		 * Keyword names.
		 * @return keywords should not be empty.
		 */
		List<String> aliases();

		/**
		 * Creates a pattern key of one key name.
		 * @param key key name.
		 * @return pattern key.
		 */
		public static PatternKey of(String key) {
			return new CustomConverterKey(List.of(key));
		}

		/**
		 * Creates a pattern key of one key and aliases.
		 * @param key key name.
		 * @param aliases other names.
		 * @return pattern key.
		 */
		public static PatternKey of(String key, String... aliases) {
			List<String> keys = new ArrayList<>();
			keys.add(Objects.requireNonNull(key));
			for (var a : aliases) {
				keys.add(Objects.requireNonNull(a));
			}
			return new CustomConverterKey(List.copyOf(keys));
		}

	}

	/**
	 * Implement this interface along with the formatter for easier registration.
	 *
	 * @see PatternRegistry#register(PatternKeyProvider)
	 */
	public interface PatternKeyProvider {

		/**
		 * Key
		 * @return key.
		 */
		PatternKey key();

	}

	/**
	 * Built-in supported pattern keys.
	 */
	@CaseChanging
	/*
	 * aliases is always assigned via List.of(...) in the constructor - genuinely
	 * immutable, just not through a type that says so statically.
	 */
	@SuppressWarnings("ImmutableEnumChecker")
	enum KeywordKey implements PatternKey {

		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#date">Date keywords</a>
		 */
		DATE("d", "date"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#micros">Micros keywords</a>
		 */
		MICROS("ms", "micros"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#relative">Relative time
		 * keywords</a>. Milliseconds elapsed since {@link PatternConfig#startTime()}.
		 */
		RELATIVE("r", "relative"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#file">File</a>
		 */
		FILE("f", "file"),
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#line">Line no</a>
		 */
		LINE("L", "line"),
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#class">Calling class</a>
		 */
		CLASS("C", "class"),
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#line">Line no</a>
		 */
		METHOD("M", "method"),
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#thread">Thread keywords</a>
		 */
		THREAD("t", "thread"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#level">Level keywords</a>
		 */
		LEVEL("level", "le", "p"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#logger">Logger keywords</a>
		 */
		LOGGER("lo", "logger", "c"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#message">Message
		 * keywords</a>
		 */
		MESSAGE("m", "msg", "message"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#mdc">MDC keywords</a>
		 */
		MDC("X", "mdc"), //
		/**
		 * RainbowGum's own percent-encoded (RFC 3986 URI query) MDC style, e.g.
		 * <code>key1=value1&amp;key2=value2</code> - unlike {@link #MDC}, which matches
		 * Logback/Log4j2's <code>%X</code> output exactly.
		 */
		ENCODED_MDC("encodedMdc"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#mdc">Property keywords</a>
		 * @see PatternConfig#propertyFunction()
		 */
		PROPERTY("property"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#ex">Throwable keywords</a>
		 */
		THROWABLE("ex", "exception", "throwable"), //
		/**
		 * <a href= "https://logback.qos.ch/manual/layouts.html#xThrowable">Extended
		 * throwable keywords</a>. Same options as {@link #THROWABLE} but also appends
		 * packaging data (jar/module and version) after each frame.
		 */
		EXTENDED_THROWABLE("xEx", "xException", "xThrowable"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#rootException">Root
		 * exception keywords</a>. Same options as {@link #EXTENDED_THROWABLE} (including
		 * packaging data) but the root cause prints first and each wrapper prints
		 * afterward, captioned "Wrapped by: " instead of "Caused by: ".
		 */
		ROOT_EXCEPTION("rEx", "rootException"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#nopex">No exception
		 * keywords</a>. Renders nothing but, like {@link #THROWABLE},
		 * {@link #EXTENDED_THROWABLE}, and {@link #ROOT_EXCEPTION}, counts as the pattern
		 * having addressed exception rendering - suppressing the automatic exception
		 * formatter that is otherwise appended to any pattern that mentions none of these
		 * keywords.
		 */
		NO_EXCEPTION("nopex", "nopexception"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#lsn">Local sequence number
		 * keywords</a>. Unlike logback's <code>%sn</code>/ <code>%sequenceNumber</code>
		 * (which requires configuring a context-wide
		 * <code>SequenceNumberGenerator</code>, a feature not implemented here) this is a
		 * simple counter local to each compiled use of <code>%lsn</code>, starting at
		 * {@link PatternConfig#sequenceNumberStart()} and incrementing once per event
		 * formatted.
		 */
		LOCAL_SEQUENCE_NUMBER("lsn"), //
		/**
		 * <a href="https://logback.qos.ch/manual/layouts.html#newline">Newline
		 * keywords</a>
		 */
		LINESEP("n");

		private final List<String> aliases;

		private KeywordKey(List<String> aliases) {
			this.aliases = aliases;
		}

		private KeywordKey(String... others) {
			this(List.of(others));
		}

		@Override
		public List<String> aliases() {
			return this.aliases;
		}

	}

	/**
	 * Built-in supported color pattern keys.
	 */
	@CaseChanging
	/*
	 * aliases is always assigned via List.of(...) in the constructor - genuinely
	 * immutable, just not through a type that says so statically.
	 */
	@SuppressWarnings("ImmutableEnumChecker")
	enum ColorKey implements PatternKey {

		/**
		 * ANSI black color
		 */
		BLACK("black"), //
		/**
		 * ANSI red color
		 */
		RED("red"), //
		/**
		 * ANSI green color
		 */
		GREEN("green"), //
		/**
		 * ANSI yellow color
		 */
		YELLOW("yellow"), //
		/**
		 * ANSI blue color
		 */
		BLUE("blue"), //
		/**
		 * ANSI magenta color
		 */
		MAGENTA("magenta"), //
		/**
		 * ANSI cyan color
		 */
		CYAN("cyan"), //
		/**
		 * ANSI white color
		 */
		WHITE("white"), //
		/**
		 * ANSI gray color
		 */
		GRAY("gray"), //
		/**
		 * ANSI boldRed color
		 */
		BOLD_RED("boldRed"), //
		/**
		 * ANSI boldGreen color
		 */
		BOLD_GREEN("boldGreen"), //
		/**
		 * ANSI boldYellow color
		 */
		BOLD_YELLOW("boldYellow"), //
		/**
		 * ANSI boldBlue color
		 */
		BOLD_BLUE("boldBlue"), //
		/**
		 * ANSI boldMagenta color
		 */
		BOLD_MAGENTA("boldMagenta"), //
		/**
		 * ANSI boldCyan color
		 */
		BOLD_CYAN("boldCyan"), //
		/**
		 * ANSI boldWhite color
		 */
		BOLD_WHITE("boldWhite"), //
		/**
		 * A special color composite key that will highlight based on logger level.
		 */
		HIGHLIGHT("highlight"), //
		;

		private final List<String> aliases;

		private ColorKey(List<String> aliases) {
			this.aliases = aliases;
		}

		private ColorKey(String... others) {
			this(List.of(others));
		}

		@Override
		public List<String> aliases() {
			return this.aliases;
		}

	}

	// config.conversionRule("clr", ColorConverter.class);
	// config.conversionRule("correlationId", CorrelationIdConverter.class);
	// config.conversionRule("esb", EnclosedInSquareBracketsConverter.class);
	// config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
	// config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class)

	/**
	 * Spring Boot patterns.
	 */
	@CaseChanging
	/*
	 * aliases is always assigned via List.of(...) in the constructor - genuinely
	 * immutable, just not through a type that says so statically.
	 */
	@SuppressWarnings("ImmutableEnumChecker")
	enum SpringBootKey implements PatternKey {

		/*
		 * TODO maybe this should be in the spring boot module only?
		 */

		/**
		 * <a href=
		 * "https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.console-output.color-coded">
		 * Spring Boot color pattern key</a>
		 *
		 */
		CLR("clr"),
		/**
		 * Spring Boot converter that surrounds property in square brackets.
		 */
		ESB("esb");

		private final List<String> aliases;

		private SpringBootKey(List<String> aliases) {
			this.aliases = aliases;
		}

		private SpringBootKey(String... others) {
			this(List.of(others));
		}

		@Override
		public List<String> aliases() {
			return this.aliases;
		}

	}

}

final class DefaultPatternRegistry implements PatternRegistry {

	private final Map<String, PatternKey> keys = new HashMap<>();

	private final Map<PatternKey, PatternFormatterFactory> converters = new HashMap<>();

	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	public DefaultPatternRegistry() {
		super();
	}

	@Override
	public void register(PatternKey key, PatternFormatterFactory factory) {
		lock.writeLock().lock();
		try {
			var f = converters.get(key);
			if (f == null) {
				for (var a : key.aliases()) {
					if (keys.containsKey(a)) {
						throw new IllegalArgumentException("Key already registered: " + a);
					}
					keys.put(a, key);
				}
			}
			converters.put(key, factory);
		}
		finally {
			lock.writeLock().unlock();
		}
	}

	@Override
	public @Nullable PatternFormatterFactory getOrNull(String key) {
		lock.readLock().lock();
		try {
			var ck = keys.get(key);
			if (ck == null)
				return null;
			return converters.get(ck);
		}
		finally {
			lock.readLock().unlock();
		}
	}

	static PatternRegistry registerDefaults(PatternRegistry registry) {
		for (KeywordKey c : KeywordKey.values()) {
			KeywordFactory keyword = switch (c) {
				case MESSAGE -> KeywordFactory.of(LogFormatter.builder().message().build());
				case DATE -> StandardKeywordFactory.DATE;
				case LEVEL -> KeywordFactory.of(LogFormatter.LevelFormatter.of());
				case LINESEP -> (fc, n) -> new LogFormatter.StaticFormatter(fc.lineSeparator());
				case LOCAL_SEQUENCE_NUMBER -> StandardKeywordFactory.LOCAL_SEQUENCE_NUMBER;
				case LOGGER -> StandardKeywordFactory.LOGGER;
				case MDC -> StandardKeywordFactory.MDC;
				case ENCODED_MDC -> StandardKeywordFactory.ENCODED_MDC;
				case MICROS -> KeywordFactory.of(LogFormatter.TimestampFormatter.ofMicros());
				case RELATIVE -> StandardKeywordFactory.RELATIVE;
				case THREAD -> KeywordFactory.of(LogFormatter.builder().threadName().build());
				case THROWABLE -> StandardKeywordFactory.THROWABLE;
				case EXTENDED_THROWABLE -> StandardKeywordFactory.EXTENDED_THROWABLE;
				case ROOT_EXCEPTION -> StandardKeywordFactory.ROOT_EXCEPTION;
				case NO_EXCEPTION -> KeywordFactory.of(LogFormatter.noop(), true);
				case CLASS -> KeywordFactory.of(CallerInfoFormatter.CLASS);
				case FILE -> KeywordFactory.of(CallerInfoFormatter.FILE);
				case LINE -> KeywordFactory.of(CallerInfoFormatter.LINE);
				case METHOD -> KeywordFactory.of(CallerInfoFormatter.METHOD);
				case PROPERTY -> StandardKeywordFactory.PROPERTY;
			};

			registry.register(c, keyword);
		}

		for (ColorKey c : ColorKey.values()) {
			var factory = switch (c) {
				case RED -> ColorCompositeFactory.RED;
				case BLACK -> ColorCompositeFactory.BLACK;
				case BLUE -> ColorCompositeFactory.BLUE;
				case CYAN -> ColorCompositeFactory.CYAN;
				case GRAY -> ColorCompositeFactory.GRAY;
				case GREEN -> ColorCompositeFactory.GREEN;
				case MAGENTA -> ColorCompositeFactory.MAGENTA;
				case WHITE -> ColorCompositeFactory.WHITE;
				case YELLOW -> ColorCompositeFactory.YELLOW;
				case BOLD_BLUE -> ColorCompositeFactory.BOLD_BLUE;
				case BOLD_CYAN -> ColorCompositeFactory.BOLD_CYAN;
				case BOLD_GREEN -> ColorCompositeFactory.BOLD_GREEN;
				case BOLD_MAGENTA -> ColorCompositeFactory.BOLD_MAGENTA;
				case BOLD_RED -> ColorCompositeFactory.BOLD_RED;
				case BOLD_WHITE -> ColorCompositeFactory.BOLD_WHITE;
				case BOLD_YELLOW -> ColorCompositeFactory.BOLD_YELLOW;
				case HIGHLIGHT -> HighlightCompositeFactory.HIGHTLIGHT;
			};
			registry.register(c, factory);
		}

		for (SpringBootKey sk : SpringBootKey.values()) {
			var factory = switch (sk) {
				case CLR -> HighlightCompositeFactory.CLR;
				case ESB -> StandardKeywordFactory.ENCLOSED_SQUARE_BRACKET;
			};
			registry.register(sk, factory);
		}

		registry.register(BareKey.BARE, BareCompositeFactory.BARE);
		return registry;
	}

	record CustomConverterKey(List<String> aliases) implements PatternKey {

	}

	enum BareKey implements PatternKey {

		BARE() {
			@Override
			public List<String> aliases() {
				return List.of("BARE");
			}

		}

	}

	// DEFAULT_CONVERTER_MAP.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);
	//
	//

	//
	// DEFAULT_CONVERTER_MAP.put("C", ClassOfCallerConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("class", ClassOfCallerConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(ClassOfCallerConverter.class.getName(), "class");
	//
	// DEFAULT_CONVERTER_MAP.put("M", MethodOfCallerConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("method", MethodOfCallerConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(MethodOfCallerConverter.class.getName(), "method");
	//
	// DEFAULT_CONVERTER_MAP.put("L", LineOfCallerConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("line", LineOfCallerConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(LineOfCallerConverter.class.getName(), "line");
	//
	// DEFAULT_CONVERTER_MAP.put("F", FileOfCallerConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("file", FileOfCallerConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(FileOfCallerConverter.class.getName(), "file");
	//
	// DEFAULT_CONVERTER_MAP.put("X", MDCConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("mdc", MDCConverter.class.getName());
	//
	// DEFAULT_CONVERTER_MAP.put("nopex",
	// NopThrowableInformationConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("nopexception",
	// NopThrowableInformationConverter.class.getName());
	//
	// DEFAULT_CONVERTER_MAP.put("cn", ContextNameConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("contextName", ContextNameConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(ContextNameConverter.class.getName(),
	// "contextName");
	//
	// DEFAULT_CONVERTER_MAP.put("caller", CallerDataConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(CallerDataConverter.class.getName(), "caller");
	//
	// DEFAULT_CONVERTER_MAP.put("marker", MarkerConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(MarkerConverter.class.getName(), "marker");
	//
	// DEFAULT_CONVERTER_MAP.put("kvp", KeyValuePairConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(KeyValuePairConverter.class.getName(), "kvp");
	//
	// DEFAULT_CONVERTER_MAP.put("property", PropertyConverter.class.getName());
	//
	//

	//
	// DEFAULT_CONVERTER_MAP.put("sn", SequenceNumberConverter.class.getName());
	// DEFAULT_CONVERTER_MAP.put("sequenceNumber",
	// SequenceNumberConverter.class.getName());
	// CONVERTER_CLASS_TO_KEY_MAP.put(SequenceNumberConverter.class.getName(),
	// "sequenceNumber");
	//
	// DEFAULT_CONVERTER_MAP.put("prefix", PrefixCompositeConverter.class.getName());

}