ServiceRegistry.java

package io.jstach.rainbowgum;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

/**
 * A simple service locator for initialization purposes and external services provided by
 * plugins. <strong> If there is no suitable <code>name</code> for the service use
 * {@value #DEFAULT_SERVICE_NAME}. </strong>
 */
public sealed interface ServiceRegistry extends AutoCloseable permits DefaultServiceRegistry {

	/**
	 * Services that do not have multiple options or need a default should use
	 * {@value #DEFAULT_SERVICE_NAME}.
	 */
	static final String DEFAULT_SERVICE_NAME = LogProperties.DEFAULT_NAME;

	/**
	 * Creates an empty service registry.
	 * @return registry.
	 */
	public static ServiceRegistry of() {
		return new DefaultServiceRegistry();
	}

	/**
	 * Puts a service.
	 * @param <T> service type.
	 * @param type type.
	 * @param name name of service.
	 * @param service service instance.
	 */
	public <T> void put(Class<T> type, String name, T service);

	/**
	 * Puts a service.
	 * @param <T> service type.
	 * @param type type.
	 * @param service service instance.
	 */
	default <T> void put(Class<T> type, T service) {
		put(type, DEFAULT_SERVICE_NAME, service);
	}

	/**
	 * Puts a service if absent.
	 * @param <T> service type.
	 * @param type type.
	 * @param name name of the service.
	 * @param supplier service instance.
	 * @return service.
	 */
	public <T> T putIfAbsent(Class<T> type, String name, Supplier<T> supplier);

	/**
	 * Puts a service if absent.
	 * @param <T> service type.
	 * @param type type.
	 * @param supplier service instance.
	 * @return service.
	 */
	default <T> T putIfAbsent(Class<T> type, Supplier<T> supplier) {
		return putIfAbsent(type, DEFAULT_SERVICE_NAME, supplier);
	}

	/**
	 * Finds a service or null.
	 * @param <T> service type
	 * @param type service class.
	 * @param name name of service.
	 * @return service or <code>null</code>.
	 */
	@SuppressWarnings("exports")
	public <T> @Nullable T findOrNull(Class<T> type, String name);

	/**
	 * Finds a service with the default name of {@value #DEFAULT_SERVICE_NAME} or null.
	 * @param <T> service type
	 * @param type service class.
	 * @return service or <code>null</code>.
	 */
	@SuppressWarnings("exports")
	default <T> @Nullable T findOrNull(Class<T> type) {
		return findOrNull(type, DEFAULT_SERVICE_NAME);
	}

	/**
	 * Finds <strong>all</strong> services of a specific type.
	 * @param <T> service type.
	 * @param type service class.
	 * @return list of services.
	 */
	public <T> List<T> find(Class<T> type);

	/**
	 * Finds <strong>all</strong> services of a specific type.
	 * @param <T> service type.
	 * @param type service class.
	 * @param consumer called on each entry with string parameter being the name.
	 */
	public <T> void forEach(Class<T> type, BiConsumer<String, T> consumer);

	/**
	 * Add a closeable to close on close in LIFO order.
	 * @param closeable closeable.
	 */
	public void onClose(AutoCloseable closeable);

	@Override
	public void close();

}

record ServiceKey(Class<?> type, String name) {
	ServiceKey {
		Objects.requireNonNull(type);
		Objects.requireNonNull(name);
	}
}

final class DefaultServiceRegistry implements ServiceRegistry {

	private final Map<ServiceKey, Object> services = new ConcurrentHashMap<>();

	private final CopyOnWriteArrayList<AutoCloseable> closeables = new CopyOnWriteArrayList<>();

	@Override
	public <T> void put(Class<T> type, String name, T service) {
		if (service == null) {
			throw new NullPointerException("service");
		}
		services.putIfAbsent(new ServiceKey(type, name), service);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> @Nullable T findOrNull(Class<T> type, String name) {
		Objects.requireNonNull(type, "type");
		Objects.requireNonNull(name, "name");
		return (T) services.get(new ServiceKey(type, name));
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> List<T> find(Class<T> type) {
		return services.entrySet().stream().filter(e -> e.getKey().type() == type).map(e -> (T) e.getValue()).toList();
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> void forEach(Class<T> type, BiConsumer<String, T> consumer) {
		services.entrySet()
			.stream()
			.filter(e -> e.getKey().type() == type)
			.forEach(e -> consumer.accept(e.getKey().name(), (T) e.getValue()));
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T putIfAbsent(Class<T> type, String name, Supplier<T> supplier) {
		var t = (T) services.computeIfAbsent(new ServiceKey(type, name), k -> {
			T v = supplier.get();
			if (v == null) {
				throw new NullPointerException("supplier returned null. type: " + type);
			}
			return v;
		});
		return t;
	}

	@Override
	public void onClose(AutoCloseable closeable) {
		closeables.addIfAbsent(closeable);
	}

	@Override
	public void close() {
		for (var c : closeables.reversed()) {
			try {
				c.close();
			}
			catch (Exception e) {
				MetaLog.error(ServiceRegistry.class,
						"Failure trying to close service registry closeable: " + c.getClass().getName(), e);
			}
		}
	}

}