LogOutputRegistry.java

package io.jstach.rainbowgum;

import java.net.URI;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

import io.jstach.rainbowgum.LogOutput.OutputProvider;
import io.jstach.rainbowgum.output.ListLogOutput;
import io.jstach.rainbowgum.output.StdErrOutputBuilder;
import io.jstach.rainbowgum.output.StdOutOutputBuilder;

/**
 * Register output providers by URI scheme.
 */
public sealed interface LogOutputRegistry extends OutputProvider permits DefaultOutputRegistry {

	/**
	 * The URI scheme for list provider.
	 */
	public static String LIST_OUTPUT_SCHEME = "list";

	/**
	 * Register a provider by {@link URI#getScheme() scheme}.
	 * @param scheme URI scheme to match for.
	 * @param provider provider for scheme.
	 */
	public void register(String scheme, OutputProvider provider);

	/**
	 * Finds an output by name.
	 * @param name output name.
	 * @return maybe an output.
	 */
	Optional<LogOutput> output(String name);

	/**
	 * Attempts to reopen all outputs of a certain type usually for log rotation. This
	 * call will block if it attempts to reopen. If reopening is already happening an
	 * empty list will be returned.
	 * @return alert events for outputs that failed to reopen, or an empty list if
	 * reopening was skipped or every output reopened successfully.
	 */
	public List<LogEvent> reopen();

	/**
	 * Attempts to flush all outputs usually for log rotation. This call will block if it
	 * attempts to flush. If flush is already happening an empty list will be returned.
	 * @return alert events for outputs that failed to flush, or an empty list if flushing
	 * was skipped or every output flushed successfully.
	 */
	public List<LogEvent> flush();

}

final class DefaultOutputRegistry implements LogOutputRegistry {

	private final Map<String, LogOutput> outputs = new ConcurrentHashMap<>();

	private final Map<String, OutputProvider> providers = new ConcurrentHashMap<>();

	private final ServiceRegistry serviceRegistry;

	private final ReentrantLock reopenLock = new ReentrantLock();

	static LogOutputRegistry of(ServiceRegistry serviceRegistry) {
		var r = new DefaultOutputRegistry(serviceRegistry);
		for (var p : StandardLogOutputProvider.values()) {
			r.register(p.scheme(), p);
		}
		return r;
	}

	public DefaultOutputRegistry(ServiceRegistry serviceRegistry) {
		this.serviceRegistry = serviceRegistry;
	}

	@Override
	public List<LogEvent> reopen() {
		return requestIO(LogAction.StandardAction.REOPEN);
	}

	@Override
	public List<LogEvent> flush() {
		return requestIO(LogAction.StandardAction.FLUSH);
	}

	private List<LogEvent> requestIO(LogAction action) {
		if (reopenLock.tryLock()) {
			try {
				return _request(action);
			}
			finally {
				reopenLock.unlock();
			}
		}
		else {
			return List.of();
		}
	}

	private List<LogEvent> _request(LogAction action) {
		/*
		 * TODO check rainbowgum is actually running.
		 */
		return Actor.act(serviceRegistry.find(LogAppender.class).stream().map(a -> InternalLogAppender.of(a)).toList(),
				action);
	}

	@Override
	public void register(String scheme, OutputProvider provider) {
		providers.put(scheme, provider);
	}

	private LogOutput _register(String name, LogOutput output) {
		if (outputs.putIfAbsent(name, output) != null) {
			throw new IllegalArgumentException("Name is already in use. name = '%s'".formatted(name));
		}
		return output;
	}

	@Override
	public Optional<LogOutput> output(String name) {
		return Optional.ofNullable(outputs.get(name));
	}

	private static URI normalize(URI uri) {
		String scheme = uri.getScheme();
		String path = uri.getPath();
		String query = uri.getRawQuery();
		if (scheme == null) {
			if (path == null) {
				throw new IllegalArgumentException("URI is not proper: " + LogRedactor.redactUri(uri));
			}
			if (path.startsWith("./")) {
				uri = Paths.get(path).toUri();
			}
			else {
				uri = URI.create(path + ":///");
			}
		}
		else if (scheme.equals("file")) {
			if (path.startsWith("/./")) {
				path = path.substring(1);
			}
			uri = Paths.get(path).toUri();
			if (query != null) {
				uri = URI.create(uri.toString() + "?" + query);
			}
		}
		return uri;
	}

	private static LogProviderRef normalize(LogProviderRef ref) {
		var uri = normalize(ref.uri());
		return new DefaultLogProviderRef(uri, ref.keyOrNull());
	}

	@Override
	public LogProvider<LogOutput> provide(LogProviderRef ref) {
		ref = normalize(ref);
		var uri = ref.uri();
		String scheme = Objects.requireNonNull(uri.getScheme());
		OutputProvider customProvider = providers.get(scheme);
		if (customProvider == null) {
			throw LogProviderRef.NotFoundException.of(ProviderModule.ComponentType.OUTPUT, scheme, uri,
					providers.keySet());
		}
		var _ref = ref;
		return (name, config) -> {
			return _register(name, customProvider.provide(_ref).provide(name, config));
		};
	}

	enum StandardLogOutputProvider implements LogOutput.OutputProvider {

		STDOUT {
			@Override
			public LogProvider<LogOutput> provide(LogProviderRef ref) {
				return (s, c) -> new StdOutOutputBuilder(s).fromProperties(c.properties(), ref).build().provide(s, c);
			}

			@Override
			public String scheme() {
				return LogOutput.STDOUT_SCHEME;
			}

		},
		STDERR {
			@Override
			public LogProvider<LogOutput> provide(LogProviderRef ref) {
				return (s, c) -> new StdErrOutputBuilder(s).fromProperties(c.properties(), ref).build().provide(s, c);
			}

			@Override
			public String scheme() {
				return LogOutput.STDERR_SCHEME;
			}

		},
		LIST {
			@Override
			public LogProvider<LogOutput> provide(LogProviderRef ref) {
				return (s, c) -> new ListLogOutput();
			}

			@Override
			public String scheme() {
				return LIST_OUTPUT_SCHEME;
			}

		};

		public abstract String scheme();

	}

}