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}