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    /**
078     * Clears every registered provider. Test support only; not part of the catalog runtime contract.
079     *
080     * <p>PUBLIC ON PURPOSE: every shipped jar is ProGuard-obfuscated under a
081     * compatibility-first policy that preserves public and protected members
082     * and renames everything else. A package-private method is therefore
083     * unreachable from the released jar, which is what the test suite runs
084     * against in the nightly obfuscated-jar gate. Keeping this public is what
085     * lets that gate execute the real shipped bytes.</p>
086     *
087     * @since 4.2.6
088     */
089    public static void clearForTesting() {
090        synchronized (MUTEX) {
091            FACTORIES.clear();
092            LOADED.set(false);
093        }
094    }
095
096    private static void ensureLoaded() {
097        if (LOADED.compareAndSet(false, true)) {
098            for (CatalogProviderFactory f : ServiceLoader.load(CatalogProviderFactory.class)) {
099                if (f.id() != null && !FACTORIES.containsKey(f.id())) {
100                    FACTORIES.put(f.id(), f);
101                }
102            }
103        }
104    }
105}