001package gudusoft.gsqlparser.lineage.v2;
002
003import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
004
005import java.io.ByteArrayOutputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.nio.charset.StandardCharsets;
009import java.security.MessageDigest;
010import java.security.NoSuchAlgorithmException;
011import java.util.Collections;
012import java.util.LinkedHashMap;
013import java.util.Map;
014import java.util.concurrent.ConcurrentHashMap;
015
016/**
017 * The SQL-LR data files this library ships: the lineage document schema, the core registry, the
018 * qualification profiles, the dependency snapshots and the identifier rule sets, at their
019 * {@code lineage-spec/} paths under {@value #BASE}.
020 *
021 * <p>A jar cannot list a directory, so {@code resources.sha256} (in {@code sha256sum} format) lists every
022 * file with its SHA-256 digest. A file is served only when it is listed, and only when its bytes have that
023 * digest. Each file is parsed once with {@link CanonicalJson#parse(byte[])} into an unmodifiable value
024 * that every caller shares.</p>
025 *
026 * @since 4.2.17
027 */
028public final class SqllrResources {
029
030    /** The classpath directory of the shipped files. */
031    public static final String BASE = "/gudusoft/gsqlparser/lineage/v2/";
032
033    /** The index file, relative to {@link #BASE}. */
034    public static final String INDEX = "resources.sha256";
035
036    public static final String LINEAGE_DOCUMENT_SCHEMA = "schema/0.1/lineage-document.schema.json";
037    public static final String CORE_REGISTRY = "registry/core-registry.json";
038    public static final String QUALIFICATION_PROFILES = "registry/qualification-profiles.json";
039
040    private static final Map<String, Object> PARSED = new ConcurrentHashMap<String, Object>();
041
042    private SqllrResources() {
043    }
044
045    /** Every shipped file (path relative to {@link #BASE}) with its lowercase hexadecimal SHA-256. */
046    public static Map<String, String> index() {
047        return Index.ENTRIES;
048    }
049
050    private static final class Index {
051        static final Map<String, String> ENTRIES = load();
052
053        private static Map<String, String> load() {
054            String text = new String(read(INDEX), StandardCharsets.UTF_8);
055            Map<String, String> entries = new LinkedHashMap<String, String>();
056            for (String line : text.split("\n")) {
057                if (line.trim().isEmpty()) {
058                    continue;
059                }
060                int space = line.indexOf("  ");
061                if (space != 64) {
062                    throw new IllegalStateException("malformed line in " + INDEX + ": " + line);
063                }
064                entries.put(line.substring(space + 2).trim(), line.substring(0, space));
065            }
066            return Collections.unmodifiableMap(entries);
067        }
068    }
069
070    /**
071     * The bytes of a shipped file.
072     *
073     * @param path a path of {@link #index()}, such as {@code registry/core-registry.json}
074     * @throws IllegalArgumentException when the file is not listed
075     * @throws IllegalStateException    when the file is missing or its digest differs from the index
076     */
077    public static byte[] bytes(String path) {
078        String expected = index().get(path);
079        if (expected == null) {
080            throw new IllegalArgumentException("not a shipped SQL-LR resource: " + path);
081        }
082        byte[] data = read(path);
083        String actual = sha256Hex(data);
084        if (!actual.equals(expected)) {
085            throw new IllegalStateException("SQL-LR resource " + path + " has sha256 " + actual + ", the index says "
086                    + expected);
087        }
088        return data;
089    }
090
091    /** A shipped file parsed under the input rules of {@code canonical-json-v1}; shared, do not modify. */
092    public static Object json(String path) {
093        Object value = PARSED.get(path);
094        if (value != null) {
095            return value;
096        }
097        synchronized (PARSED) {
098            value = PARSED.get(path);
099            if (value == null) {
100                value = CanonicalJson.immutableCopy(CanonicalJson.parse(bytes(path)));
101                PARSED.put(path, value);
102            }
103            return value;
104        }
105    }
106
107    /** {@code schema/0.1/lineage-document.schema.json}. */
108    public static Object lineageDocumentSchema() {
109        return json(LINEAGE_DOCUMENT_SCHEMA);
110    }
111
112    /** {@code registry/core-registry.json}. */
113    public static Object coreRegistry() {
114        return json(CORE_REGISTRY);
115    }
116
117    /** {@code registry/qualification-profiles.json}. */
118    public static Object qualificationProfiles() {
119        return json(QUALIFICATION_PROFILES);
120    }
121
122    /** The identifier rule set {@code identifier-rules/<dialect>/rules-<version>.json}. */
123    public static Object identifierRules(String dialect, int ruleSetVersion) {
124        return json("identifier-rules/" + dialect + "/rules-" + ruleSetVersion + ".json");
125    }
126
127    /** The lowercase hexadecimal SHA-256 of {@code data}. */
128    public static String sha256Hex(byte[] data) {
129        byte[] digest;
130        try {
131            digest = MessageDigest.getInstance("SHA-256").digest(data);
132        } catch (NoSuchAlgorithmException e) {
133            throw new IllegalStateException(e);
134        }
135        StringBuilder out = new StringBuilder(64);
136        for (byte b : digest) {
137            out.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16));
138        }
139        return out.toString();
140    }
141
142    private static byte[] read(String path) {
143        InputStream in = SqllrResources.class.getResourceAsStream(BASE + path);
144        if (in == null) {
145            throw new IllegalStateException("missing SQL-LR resource " + BASE + path);
146        }
147        try {
148            ByteArrayOutputStream out = new ByteArrayOutputStream();
149            byte[] buffer = new byte[8192];
150            int n;
151            while ((n = in.read(buffer)) >= 0) {
152                out.write(buffer, 0, n);
153            }
154            return out.toByteArray();
155        } catch (IOException e) {
156            throw new IllegalStateException("cannot read SQL-LR resource " + BASE + path, e);
157        } finally {
158            try {
159                in.close();
160            } catch (IOException ignored) {
161                // nothing to do
162            }
163        }
164    }
165}