001package gudusoft.gsqlparser.catalog.input;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.LinkedHashMap;
006import java.util.List;
007import java.util.Map;
008import java.util.ServiceLoader;
009import java.util.concurrent.atomic.AtomicBoolean;
010
011/**
012 * Look-up table for {@link CatalogInputReaderFactory}s registered via
013 * {@code META-INF/services/gudusoft.gsqlparser.catalog.input.CatalogInputReaderFactory} or
014 * programmatically via {@link #register(CatalogInputReaderFactory)}.
015 *
016 * <p>Plan §7.1 / §13.1. Process-global registry; lazily initialized from the
017 * {@link ServiceLoader}; programmatic registration takes precedence on
018 * {@link CatalogInputKind} collision.</p>
019 */
020public final class CatalogInputReaders {
021
022    private static final Object MUTEX = new Object();
023    private static final Map<CatalogInputKind, CatalogInputReaderFactory> FACTORIES =
024        new LinkedHashMap<CatalogInputKind, CatalogInputReaderFactory>();
025    private static final AtomicBoolean LOADED = new AtomicBoolean(false);
026
027    private CatalogInputReaders() {
028        // Static utility — no instances.
029    }
030
031    public static void register(CatalogInputReaderFactory factory) {
032        if (factory == null) {
033            throw new IllegalArgumentException("CatalogInputReaders.register: factory is required");
034        }
035        if (factory.kind() == null) {
036            throw new IllegalArgumentException(
037                "CatalogInputReaders.register: factory kind may not be null");
038        }
039        synchronized (MUTEX) {
040            ensureLoaded();
041            FACTORIES.put(factory.kind(), factory);
042        }
043    }
044
045    /**
046     * Unique-match selector. Walks all registered factories and returns the one whose
047     * reader {@code supports(...)} the source. Throws {@link CatalogInputException} on
048     * zero or multiple matches; the exception message lists the candidates so callers
049     * can disambiguate by setting {@link CatalogInputSource#declaredKind()}.
050     */
051    public static CatalogInputReader forSource(CatalogInputSource source,
052                                               CatalogLoadOptions options)
053            throws CatalogInputException {
054        if (source == null) {
055            throw new CatalogInputException(
056                "CatalogInputReaders.forSource: source is required");
057        }
058        synchronized (MUTEX) {
059            ensureLoaded();
060            // Fast path: caller declared a kind that we've registered.
061            if (source.declaredKind() != null) {
062                CatalogInputReaderFactory direct = FACTORIES.get(source.declaredKind());
063                if (direct != null) {
064                    CatalogInputReader r = direct.create();
065                    if (r.supports(source, options)) {
066                        return r;
067                    }
068                }
069            }
070            // Generic walk: collect every reader that claims support.
071            List<CatalogInputReader> matches = new ArrayList<CatalogInputReader>();
072            List<CatalogInputKind> matchedKinds = new ArrayList<CatalogInputKind>();
073            for (CatalogInputReaderFactory f : FACTORIES.values()) {
074                CatalogInputReader r = f.create();
075                if (r.supports(source, options)) {
076                    matches.add(r);
077                    matchedKinds.add(f.kind());
078                }
079            }
080            if (matches.isEmpty()) {
081                throw new CatalogInputException(
082                    "No CatalogInputReader matches source=" + source
083                        + "; registered kinds=" + new ArrayList<CatalogInputKind>(FACTORIES.keySet()));
084            }
085            if (matches.size() > 1) {
086                throw new CatalogInputException(
087                    "Ambiguous CatalogInputReader match for source=" + source
088                        + "; candidates=" + matchedKinds
089                        + ". Disambiguate by setting CatalogInputSource.declaredKind.");
090            }
091            return matches.get(0);
092        }
093    }
094
095    public static List<CatalogInputReaderFactory> registered() {
096        synchronized (MUTEX) {
097            ensureLoaded();
098            return Collections.unmodifiableList(
099                new ArrayList<CatalogInputReaderFactory>(FACTORIES.values()));
100        }
101    }
102
103    /** Test-only hook: discard all registrations and reset the lazy-load flag. */
104    static void clearForTesting() {
105        synchronized (MUTEX) {
106            FACTORIES.clear();
107            LOADED.set(false);
108        }
109    }
110
111    private static void ensureLoaded() {
112        if (LOADED.compareAndSet(false, true)) {
113            for (CatalogInputReaderFactory f : ServiceLoader.load(CatalogInputReaderFactory.class)) {
114                if (f.kind() != null && !FACTORIES.containsKey(f.kind())) {
115                    FACTORIES.put(f.kind(), f);
116                }
117            }
118        }
119    }
120}