001package gudusoft.gsqlparser.catalog.runtime;
002
003import gudusoft.gsqlparser.catalog.diagnostic.CatalogException;
004
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.LinkedHashMap;
008import java.util.List;
009import java.util.Map;
010import java.util.ServiceLoader;
011import java.util.concurrent.atomic.AtomicBoolean;
012
013/**
014 * Look-up table for {@link CatalogProviderFactory}s registered via
015 * {@code META-INF/services/gudusoft.gsqlparser.catalog.runtime.CatalogProviderFactory} or
016 * programmatically via {@link #register(CatalogProviderFactory)}.
017 *
018 * <p>Plan §7.2 / §13.1. The registry is process-global, lazily initialized from the
019 * {@link ServiceLoader}, and synchronized on every mutator. Programmatic registration
020 * replaces any previous registration with the same {@link CatalogProviderId}.</p>
021 */
022public final class CatalogProviderRegistry {
023
024    private static final Object MUTEX = new Object();
025    private static final Map<CatalogProviderId, CatalogProviderFactory> FACTORIES =
026        new LinkedHashMap<CatalogProviderId, CatalogProviderFactory>();
027    private static final AtomicBoolean LOADED = new AtomicBoolean(false);
028
029    private CatalogProviderRegistry() {
030        // Static utility — no instances.
031    }
032
033    public static void register(CatalogProviderFactory factory) {
034        if (factory == null) {
035            throw new IllegalArgumentException("CatalogProviderRegistry.register: factory is required");
036        }
037        if (factory.id() == null) {
038            throw new IllegalArgumentException(
039                "CatalogProviderRegistry.register: factory id may not be null");
040        }
041        synchronized (MUTEX) {
042            ensureLoaded();
043            FACTORIES.put(factory.id(), factory);
044        }
045    }
046
047    /**
048     * Look up the factory for {@code id} and create a new {@link CatalogProvider}. Throws
049     * {@link CatalogException} when no factory matches — callers should ensure the
050     * factory is registered before invoking.
051     */
052    public static CatalogProvider create(CatalogProviderId id) {
053        if (id == null) {
054            throw new IllegalArgumentException("CatalogProviderRegistry.create: id is required");
055        }
056        synchronized (MUTEX) {
057            ensureLoaded();
058            CatalogProviderFactory factory = FACTORIES.get(id);
059            if (factory == null) {
060                throw new CatalogException(
061                    "No CatalogProviderFactory registered for id=" + id
062                        + "; registered=" + new ArrayList<CatalogProviderId>(FACTORIES.keySet()));
063            }
064            return factory.create();
065        }
066    }
067
068    public static List<CatalogProviderFactory> registered() {
069        synchronized (MUTEX) {
070            ensureLoaded();
071            return Collections.unmodifiableList(
072                new ArrayList<CatalogProviderFactory>(FACTORIES.values()));
073        }
074    }
075
076    /** Test-only hook: discard all registrations and reset the lazy load flag. */
077    static void clearForTesting() {
078        synchronized (MUTEX) {
079            FACTORIES.clear();
080            LOADED.set(false);
081        }
082    }
083
084    private static void ensureLoaded() {
085        if (LOADED.compareAndSet(false, true)) {
086            for (CatalogProviderFactory f : ServiceLoader.load(CatalogProviderFactory.class)) {
087                if (f.id() != null && !FACTORIES.containsKey(f.id())) {
088                    FACTORIES.put(f.id(), f);
089                }
090            }
091        }
092    }
093}