001package gudusoft.gsqlparser.sqlenv.parser;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.IdentifierProfile;
005import gudusoft.gsqlparser.sqlenv.TSQLEnv;
006import gudusoft.gsqlparser.sqlenv.parser.grabit.GrabitSQLEnv;
007import gudusoft.gsqlparser.sqlenv.parser.grabit.MultipleGrabitSQLEnv;
008import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
009import gudusoft.gsqlparser.sqlenv.parser.sharded.SqlflowShardedSQLEnv;
010import gudusoft.gsqlparser.sqlenv.parser.sqldep.MultipleSQLDepSQLEnv;
011import gudusoft.gsqlparser.sqlenv.parser.sqldep.SQLDepSQLEnv;
012import gudusoft.gsqlparser.sqlenv.parser.sqlflow.SqlflowSQLEnv;
013import gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader;
014import gudusoft.gsqlparser.util.SQLUtil;
015import gudusoft.gsqlparser.util.json.JSON;
016
017import java.io.File;
018import java.io.IOException;
019import java.nio.charset.StandardCharsets;
020import java.nio.file.Files;
021import java.nio.file.Path;
022import java.nio.file.Paths;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029@SuppressWarnings("rawtypes")
030public class TJSONSQLEnvParser implements TSQLEnvParser {
031        
032        private String defaultServer;
033        private String defaultDatabase;
034        private String defaultSchema;
035        
036        /**
037         * The identifier configuration every environment this parser builds is indexed with; null
038         * means each environment uses its own vendor's default.
039         */
040        private IdentifierProfile identifierProfile;
041
042        /**
043         * Configure the identifier rules the parsed environments register their metadata under.
044         *
045         * <p>Set it before {@link #parseSQLEnv}. A metadata file that declares its own
046         * {@code dbVendor} is built with that vendor's default instead, since a profile is a
047         * configuration of one dialect ({@link IdentifierProfile#forVendorOr}).
048         *
049         * @param identifierProfile the analysis's configuration, or null for vendor defaults
050         * @return this parser
051         */
052        public TJSONSQLEnvParser withIdentifierProfile(IdentifierProfile identifierProfile) {
053                this.identifierProfile = identifierProfile;
054                return this;
055        }
056
057        public TJSONSQLEnvParser(String defaultServer, String defaultDatabase, String defaultSchema) {
058                this.defaultServer = defaultServer;
059                this.defaultDatabase = defaultDatabase;
060                this.defaultSchema = defaultSchema;
061        }
062
063        public TSQLEnv[] parseSQLEnv(EDbVendor vendor, String sql) {
064                return parseSQLEnv(vendor, sql, null);
065        }
066
067        /**
068         * @param explicitBaseDir directory to resolve a sharded manifest's relative
069         *        catalog/source paths against, used when {@code sql} is inline manifest
070         *        CONTENT (which carries no location of its own). Ignored when {@code sql}
071         *        is a file path, since the path already yields the base directory.
072         */
073        public TSQLEnv[] parseSQLEnv(EDbVendor vendor, String sql, String explicitBaseDir) {
074                if (SQLUtil.isEmpty(sql))
075                        return null;
076
077                String trimSQL = sql.trim();
078
079                // Check if input is a file path (for sqlflow-sharded format). For inline
080                // content, fall back to the caller-supplied base directory so relative
081                // catalog paths still resolve.
082                String baseDir = explicitBaseDir;
083                String jsonContent = trimSQL;
084
085                if (!trimSQL.startsWith("{") && !trimSQL.startsWith("[")) {
086                        // Try to interpret as a file path
087                        File file = new File(trimSQL);
088                        if (file.exists() && file.isFile()) {
089                                Path filePath = file.toPath();
090                                Path parentDir = filePath.getParent();
091                                baseDir = (parentDir != null) ? parentDir.toString() : "";
092
093                                try {
094                                        jsonContent = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8).trim();
095                                } catch (IOException e) {
096                                        Logger.getLogger(TJSONSQLEnvParser.class.getName())
097                                                        .log(Level.WARNING, "Failed to read file: " + trimSQL, e);
098                                        return null;
099                                }
100                        }
101                }
102
103                if (jsonContent.startsWith("{") && jsonContent.endsWith("}")) {
104                        return getJSONSQLEnv(vendor, jsonContent, baseDir);
105                }
106
107                if (jsonContent.startsWith("[") && jsonContent.endsWith("]")) {
108                        return getMultipleJSONSQLEnv(vendor, jsonContent);
109                }
110
111                return null;
112        }
113
114        private TSQLEnv[] getMultipleJSONSQLEnv(EDbVendor vendor, String trimSQL) {
115                try {
116                        List json = (List) JSON.parseObject(trimSQL);
117                        if (trimSQL.indexOf("createdBy") != -1) {
118                                if (trimSQL.toLowerCase(java.util.Locale.ROOT).indexOf("sqldep") != -1) {
119                                        return new TSQLEnv[] { new MultipleSQLDepSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) };
120                                }
121                                if (trimSQL.toLowerCase(java.util.Locale.ROOT).indexOf("grabit") != -1) {
122                                        return new TSQLEnv[] { new MultipleGrabitSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) };
123                                }
124                        }
125                } catch (Exception e) {
126                        Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING, "Parse json failed.", e);
127                }
128                return null;
129        }
130
131        private TSQLEnv[] getJSONSQLEnv(EDbVendor vendor, String trimSQL, String baseDir) {
132                try {
133                        Map json = (Map) JSON.parseObject(trimSQL);
134                        // Route on the authoritative `format` field BEFORE the createdBy
135                        // product brand. A sqlflow-sharded manifest carries the exporter
136                        // brand (SQLdep/grabit/sqlflow) in createdBy, so a brand-first check
137                        // sent a SQLdep-branded sharded manifest to the legacy SQLDepSQLEnv
138                        // and silently lost the external catalog used for column resolution.
139                        if (MetadataReader.isSqlflowSharded(trimSQL)) {
140                                if (!MetadataReader.isSupportedSqlflowSharded(trimSQL)) {
141                                        // Unknown future formatVersion: do not load a catalog we may
142                                        // mis-read. No lineage catalog is better than a wrong one.
143                                        Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING,
144                                                        "Unsupported sqlflow-sharded formatVersion "
145                                                                        + MetadataReader.shardedFormatVersion(trimSQL)
146                                                                        + "; this build supports up to "
147                                                                        + MetadataReader.SUPPORTED_SHARDED_FORMAT_VERSION
148                                                                        + ". SQLEnv catalog not loaded.");
149                                        return null;
150                                }
151                                List servers = (List) json.get("servers");
152                                if (servers == null)
153                                        return null;
154                                // Declared once, at the manifest root, for the manifest and all its shards.
155                                IdentifierInputForm shardedNameForm = TSQLEnv.parseNameForm(
156                                                json.get("nameForm"), IdentifierInputForm.SQL_LEXICAL);
157                                List<TSQLEnv> sqlEnvs = new ArrayList<TSQLEnv>();
158                                for (int i = 0; i < servers.size(); i++) {
159                                        Map server = (Map) servers.get(i);
160                                        String dbVendor = (String) server.get("dbVendor");
161                                        if (SQLUtil.isEmpty(dbVendor)) {
162                                                sqlEnvs.add(new SqlflowShardedSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, server, baseDir, shardedNameForm, identifierProfile));
163                                        } else {
164                                                sqlEnvs.add(new SqlflowShardedSQLEnv(defaultServer, defaultDatabase, defaultSchema, EDbVendor.valueOf(dbVendor), server, baseDir, shardedNameForm, identifierProfile));
165                                        }
166                                }
167                                return sqlEnvs.toArray(new TSQLEnv[0]);
168                        }
169                        if (json.containsKey("createdBy")) {
170                                String createdBy = (String) json.get("createdBy");
171                                if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("sqldep") != -1) {
172                                        TSQLEnv.requireSqlLexicalNameForm(json.get("nameForm"), "SQLDep");
173                                        return new TSQLEnv[] { new SQLDepSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) };
174                                }
175                                if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("grabit") != -1) {
176                                        TSQLEnv.requireSqlLexicalNameForm(json.get("nameForm"), "Grabit");
177                                        return new TSQLEnv[] { new GrabitSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) };
178                                }
179                                if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("sqlflow") != -1) {
180                                        {
181                                                List servers = (List) json.get("servers");
182                                                if (servers == null)
183                                                        return null;
184                                                // One declaration per file: a supplier may send SQL spellings or catalog names,
185                                                // and the only thing that can be asked of them is to be consistent within a
186                                                // file. This format defaults to SQL_LEXICAL, which is what every existing file
187                                                // (and the sqlflow-ingester exporter) uses.
188                                                IdentifierInputForm nameForm = TSQLEnv.parseNameForm(
189                                                                json.get("nameForm"), IdentifierInputForm.SQL_LEXICAL);
190                                                List<TSQLEnv> sqlEnvs = new ArrayList<TSQLEnv>();
191                                                for (int i = 0; i < servers.size(); i++) {
192                                                        Map server = (Map) servers.get(i);
193                                                        String dbVendor = (String) server.get("dbVendor");
194                                                        if (SQLUtil.isEmpty(dbVendor)) {
195                                                                sqlEnvs.add(new SqlflowSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, server, nameForm, identifierProfile));
196                                                        } else {
197                                                                sqlEnvs.add(new SqlflowSQLEnv(defaultServer, defaultDatabase, defaultSchema, EDbVendor.valueOf(dbVendor), server, nameForm, identifierProfile));
198                                                        }
199                                                }
200                                                return sqlEnvs.toArray(new TSQLEnv[0]);
201                                        }
202                                }
203                        }
204                } catch (TSQLEnv.InvalidNameFormException e) {
205                        // Not swallowed: see InvalidNameFormException.
206                        throw e;
207                } catch (Exception e) {
208                        Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING, "Parse json failed.", e);
209                }
210                return null;
211        }
212}