001/*
002MIT License
003
004Copyright (c) 2020 FBSQL Team
005
006Permission is hereby granted, free of charge, to any person obtaining a copy
007of this software and associated documentation files (the "Software"), to deal
008in the Software without restriction, including without limitation the rights
009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010copies of the Software, and to permit persons to whom the Software is
011furnished to do so, subject to the following conditions:
012
013The above copyright notice and this permission notice shall be included in all
014copies or substantial portions of the Software.
015
016THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022SOFTWARE.
023
024Home:   https://fbsql.github.io
025E-Mail: fbsql.team@gmail.com
026*/
027
028package org.fbsql.servlet;
029
030import java.io.File;
031import java.io.IOException;
032import java.io.StringReader;
033import java.nio.file.Files;
034import java.nio.file.Path;
035import java.nio.file.Paths;
036import java.security.NoSuchAlgorithmException;
037import java.util.ArrayList;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Locale;
041import java.util.Map;
042
043import javax.servlet.ServletConfig;
044
045import org.fbsql.antlr4.parser.ParseStmtConnectTo;
046import org.fbsql.antlr4.parser.ParseStmtConnectTo.StmtConnectTo;
047import org.fbsql.antlr4.parser.ParseStmtDeclareProcedure;
048import org.fbsql.antlr4.parser.ParseStmtDeclareProcedure.StmtDeclareProcedure;
049import org.fbsql.antlr4.parser.ParseStmtDeclareStatement;
050import org.fbsql.antlr4.parser.ParseStmtDeclareStatement.StmtDeclareStatement;
051import org.fbsql.antlr4.parser.ParseStmtInclude;
052import org.fbsql.antlr4.parser.ParseStmtScheduleAt;
053import org.fbsql.antlr4.parser.ParseStmtScheduleAt.StmtScheduleAt;
054import org.fbsql.antlr4.parser.ParseStmtSwitchTo;
055import org.fbsql.antlr4.parser.ParseStmtSwitchTo.StmtSwitchTo;
056import org.h2.util.ScriptReader;
057import org.springframework.jdbc.core.SqlParameter;
058import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
059import org.springframework.jdbc.core.namedparam.NamedParameterUtils;
060import org.springframework.jdbc.core.namedparam.ParsedSql;
061
062/**
063 * Provides utility methods for parsing SQL scripts and statements.
064 * 
065 * Various utility methods might used to parse SQL scripts or particular SQL statements.
066 * Supports syntax of all existing (February 2020) SQL standards. 
067*/
068public class SqlParseUtils {
069
070        /**
071         * Special FBSQL statements that
072         * can be used only in «init.sql» script
073         */
074        public static final String SPECIAL_STATEMENT_CONNECT_TO        = canonizeSql("CONNECT TO");        // Connect to database instance (can be used only in «init.sql» script)
075        public static final String SPECIAL_STATEMENT_SWITCH_TO         = canonizeSql("SWITCH TO");         // Switch connection to database instance (can be used only in «init.sql» script)
076        public static final String SPECIAL_STATEMENT_DECLARE_PROCEDURE = canonizeSql("DECLARE PROCEDURE"); // Declare non native stored procedure written in one of JVM languages
077        public static final String SPECIAL_STATEMENT_SCHEDULE          = canonizeSql("SCHEDULE");          // Add scheduled stored procedure (can be used only in «init.sql» script)
078        public static final String SPECIAL_STATEMENT_DECLARE_STATEMENT = canonizeSql("DECLARE STATEMENT"); // Expose corresponding native SQL statement to frontend
079        public static final String SPECIAL_STATEMENT_INCLUDE           = canonizeSql("INCLUDE");           // Include script file(s) (can be used only in «init.sql» script)
080        public static final String SPECIAL_STATEMENT_CALL              = canonizeSql("CALL");              // CALL statement
081
082        private static final String INIT_SQL_FILE_EXT        = "init.sql";
083        /**
084         * SQL statement separator: «;»
085         */
086        private static final String STATEMENT_SEPARATOR      = ";";
087        /**
088         * Multiline line comment start: «/*»
089         */
090        private static final String MULTI_LINE_COMMENT_START = "/*";
091
092        /**
093         * Multiline comment stop: «*​/»
094         */
095        private static final String MULTI_LINE_COMMENT_STOP = "*/";
096
097        /**
098         * Single line comment start: «--»
099         */
100        private static final String SINGLE_LINE_COMMENT_START = "--";
101
102        /**
103         * Single line comment stop: (Caret return)
104         */
105        private static final String SINGLE_LINE_COMMENT_STOP = "\n";
106
107        /**
108         * Single quote: «'»
109         */
110        private static final char Q1 = '\'';
111
112        /**
113         * Double quote: «"»
114         */
115        private static final char Q2 = '"';
116
117        /**
118         * Returns the index within  SQL  string of the first occurrence of
119         * the specified token.
120         * If a token with value <code>token</code> occurs in the character
121         * sequence represented by  SQL  string  <code>sql</code>, then the
122         * index of the first such occurrence is returned.
123         * This index is the smallest value <i>i</i> such that:
124         * <blockquote><pre>
125         * sql.charAt(<i>i</i>) == token[0]
126         * </pre></blockquote>
127         * is true.
128         *
129         * @param   sql a SQL string.
130         * @param   token to search
131         * @return  the index of the first occurrence of the token in the
132         *          SQL string, or
133         *          <code>-1</code> if the token does not occur.
134         */
135        public static int indexOf(String sql, String token) {
136                boolean inQ1     = false; // in single quotes flag
137                int     inQ1_len = 0;     // counter to count characters inside single quotes
138                //
139                boolean inQ2     = false; // in double quotes flag
140                int     inQ2_len = 0;     // counter to count characters inside double quotes
141                //
142                int savedTokenOffset = -1;
143                int k                = 0; // to iterate inside token
144                //
145                for (int i = 0; i < sql.length(); i++) {
146                        char c = sql.charAt(i);
147                        if (inQ1) {
148                                if (c == Q1) {
149                                        if ((i != 0 && sql.charAt(i - 1) != Q1) || inQ1_len == 0) { // reset single quotes
150                                                inQ1     = false;
151                                                inQ1_len = 0;
152                                        }
153                                } else
154                                        inQ1_len++;
155                        } else if (inQ2) {
156                                if (c == Q2) {
157                                        if ((i != 0 && sql.charAt(i - 1) != Q2) || inQ2_len == 0) { // reset double quotes
158                                                inQ2     = false;
159                                                inQ2_len = 0;
160                                        }
161                                } else
162                                        inQ2_len++;
163                        } else {
164                                if (c == Q1)
165                                        inQ1 = true;
166                                else if (c == Q2)
167                                        inQ2 = true;
168                                else {
169                                        char tc = token.charAt(k);
170                                        if (c == tc) {
171                                                if (k == 0) {
172                                                        savedTokenOffset = i;
173                                                        if (k == token.length() - 1)
174                                                                return savedTokenOffset;
175                                                        else
176                                                                k++;
177                                                } else if (k == token.length() - 1)
178                                                        return savedTokenOffset;
179                                                else
180                                                        k++;
181                                        } else {
182                                                savedTokenOffset = -1;
183                                                k                = 0;
184                                        }
185                                }
186                        }
187                }
188                if (k == token.length())
189                        return savedTokenOffset;
190                return -1;
191        }
192
193        /**
194         * Parse CONNECT statement
195         *
196         * @param sql  - CONNECT statement
197         * @param info - ConnectionInfo Transfer object
198         */
199        public static StmtConnectTo parseConnectStatement(ServletConfig servletConfig, String sql) {
200                sql = stripComments(sql).trim();
201                sql = sql.replace('\n', ' ');
202                sql = sql.replace('\r', ' ');
203                sql = processStatement(sql);
204
205                return new ParseStmtConnectTo().parse(servletConfig, sql);
206        }
207
208        /**
209         * Parse DECLARE PROCEDURE statement
210         *
211         * @param sql           - DECLARE PROCEDURE statement
212         * @param proceduresMap - procedures Map for specific stored procedure name
213         */
214        public static void parseDeclareProcedureStatement(ServletConfig servletConfig, String sql, Map<String /* stored procedure name */, NonNativeProcedure> proceduresMap) {
215                sql = stripComments(sql).trim();
216                sql = sql.replace('\n', ' ');
217                sql = sql.replace('\r', ' ');
218                sql = processStatement(sql);
219
220                StmtDeclareProcedure stmtDeclareProcedure = new ParseStmtDeclareProcedure().parse(sql);
221                stmtDeclareProcedure.procedure = stmtDeclareProcedure.procedure.toUpperCase(Locale.ENGLISH);
222
223                String             storedProcedureName = stmtDeclareProcedure.procedure.toUpperCase(Locale.ENGLISH);
224                NonNativeProcedure nonNativeProcedure  = stmtDeclareProcedure.nonNativeProcedure;                   // <class name> + <::> + <method name>
225
226                proceduresMap.put(storedProcedureName, nonNativeProcedure);
227        }
228
229        /**
230         * Parse EXPOSE statement
231         *
232         * @param sql - EXPOSE statement
233         * @throws NoSuchAlgorithmException 
234         */
235        public static StmtDeclareStatement parseExposeStatement(ServletConfig servletConfig, String sql) {
236                sql = stripComments(sql).trim();
237                sql = sql.replace('\n', ' ');
238                sql = sql.replace('\r', ' ');
239                sql = processStatement(sql);
240
241                ParseStmtDeclareStatement parseStmtDeclareStatement = new ParseStmtDeclareStatement();
242                StmtDeclareStatement      stmtDeclareStatement      = parseStmtDeclareStatement.parse(sql);
243
244                stmtDeclareStatement.statement = processStatement(stmtDeclareStatement.statement);
245                return stmtDeclareStatement;
246        }
247
248        /**
249         * Parse SCHEDULE statement
250         *
251         * @param sql           - SCHEDULE statement
252         * @param schedulersMap - Schedulers Map for specific SQL stored procedures
253         */
254        public static void parseScheduleStatement(ServletConfig servletConfig, String sql, Map<String /* Cron expression */, List<String /* Scheduled stored procedure name */ >> schedulersMap) {
255                sql = stripComments(sql).trim();
256                sql = sql.replace('\n', ' ');
257                sql = sql.replace('\r', ' ');
258                sql = processStatement(sql);
259
260                StmtScheduleAt scheduleAt          = new ParseStmtScheduleAt().parse(sql);
261                String         storedProcedureName = scheduleAt.procedure;
262                String         cronExpression      = scheduleAt.cronExpression;
263
264                List<String /* Scheduled stored procedure name */> sqlStatementNames = schedulersMap.get(cronExpression);
265                if (sqlStatementNames == null) {
266                        sqlStatementNames = new ArrayList<>();
267                        schedulersMap.put(cronExpression, sqlStatementNames);
268                }
269                sqlStatementNames.add(storedProcedureName);
270        }
271
272        /**
273         * Extract particular clause value from SQL statement
274         * 
275         * @param sql    - source SQL statement (trimmed)
276         * @param clause - clause to search (trimmed)
277         * @return       - clause value
278         */
279        static List<Object> extractClauseAsList(ServletConfig servletConfig, String sql, String clause) {
280                String s = extractClause(servletConfig, sql, clause);
281                if (s == null)
282                        return null;
283                String[]     arr    = s.split(",");
284                List<Object> values = new ArrayList<>(arr.length);
285                for (int i = 0; i < arr.length; i++) {
286                        String val = arr[i].trim();
287                        if (val.isEmpty())
288                                values.add(null);
289                        else if (val.startsWith("'") || val.startsWith("\""))
290                                values.add(val.substring(1, val.length() - 1));
291                        else if (val.toLowerCase(Locale.ENGLISH).equals("null"))
292                                values.add(null);
293                        else if (val.toLowerCase(Locale.ENGLISH).equals("true"))
294                                values.add(true);
295                        else if (val.toLowerCase(Locale.ENGLISH).equals("false"))
296                                values.add(false);
297                        else if (val.charAt(0) == '-' || Character.isDigit(val.charAt(0)))
298                                values.add(Double.parseDouble(val));
299                        else
300                                values.add(val);
301                }
302                return values;
303        }
304
305        /**
306         * Extract particular clause value from SQL statement
307         * 
308         * @param sql    - source SQL statement (trimmed)
309         * @param clause - clause to search (trimmed)
310         * @return       - clause value
311         */
312        static List<String> extractClauseAsListOfStrings(ServletConfig servletConfig, String sql, String clause) {
313                String s = extractClause(servletConfig, sql, clause);
314                if (s == null)
315                        return null;
316                List<Object> values    = extractClauseAsList(servletConfig, sql, clause);
317                List<String> strValues = new ArrayList<>(values.size());
318                for (int i = 0; i < values.size(); i++) {
319                        Object val = values.get(i);
320                        if (val instanceof String) {
321                                String v = (String) val;
322                                if (!v.isEmpty())
323                                        strValues.add(v);
324                        }
325                }
326                return strValues;
327        }
328
329        /**
330         * Extract particular clause value from SQL statement
331         * 
332         * @param sql    - source SQL statement (trimmed)
333         * @param clause - clause to search (trimmed)
334         * @return       - clause value
335         */
336        public static String extractClauseAsString(ServletConfig servletConfig, String sql, String clause) {
337                String s = extractClause(servletConfig, sql, clause);
338                if (s == null)
339                        return null;
340                return s;
341        }
342
343        /**
344         * Extract particular clause value from SQL statement
345         * 
346         * @param sql    - source SQL statement (trimmed)
347         * @param clause - clause to search (trimmed)
348         * @return       - clause value
349         */
350        static Integer extractClauseAsInt(ServletConfig servletConfig, String sql, String clause) {
351                String s = extractClause(servletConfig, sql, clause);
352                if (s == null)
353                        return null;
354                return Integer.parseInt(s);
355        }
356
357        /**
358         * Extract particular clause value from SQL statement
359         * 
360         * @param sql    - source SQL statement (trimmed)
361         * @param clause - clause to search (trimmed)
362         * @return       - clause value
363         */
364        static boolean extractClauseAsBoolean(ServletConfig servletConfig, String sql, String clause) {
365                String s = extractClause(servletConfig, sql, clause);
366                if (s == null)
367                        return false;
368                return Boolean.parseBoolean(s);
369        }
370
371        /**
372         * Extract particular clause value from SQL statement
373         * 
374         * @param sql    - source SQL statement (trimmed)
375         * @param clause - clause to search (trimmed)
376         * @return       - clause value
377         */
378        private static String extractClause(ServletConfig servletConfig, String sql, String clause) {
379                String upperSql    = sql.toUpperCase(Locale.ENGLISH);
380                String upperClause = clause.toUpperCase(Locale.ENGLISH);
381                while (true) {
382                        int pos = indexOf(upperSql, upperClause);
383                        if (pos == -1) {
384                                if (servletConfig == null)
385                                        return null;
386                                String value = servletConfig.getInitParameter(upperClause);
387                                if (value == null || value.trim().isEmpty())
388                                        return null;
389                                return value;
390                        }
391
392                        if (pos != 0) {
393                                char prevChar = sql.charAt(pos - 1);
394                                if (!(prevChar == ' ' || prevChar == '\t' || prevChar == '/' || prevChar == '\'' || prevChar == '"' || prevChar == ')')) { // previous clause must be ended
395                                        sql      = sql.substring(pos + 1);
396                                        upperSql = upperSql.substring(pos + 1);
397                                        continue;
398                                }
399                        }
400
401                        int nextCharPos = pos + clause.length();
402                        if (nextCharPos <= sql.length() - 1) {
403                                char nextChar = sql.charAt(nextCharPos);
404                                if (!(nextChar == ' ' || nextChar == '\t' || nextChar == '/' || nextChar == '\'' || nextChar == '"' || nextChar == '(')) { // next clause must be ended
405                                        sql      = sql.substring(pos + 1);
406                                        upperSql = upperSql.substring(pos + 1);
407                                        continue;
408                                }
409                        }
410
411                        String value = sql.substring(pos + clause.length()).trim();
412                        char   quote = value.charAt(0);                            // get single «'» or double «"» quote
413                        if (quote == Q1 || quote == Q2) { // string
414                                pos = value.indexOf(quote, 1);
415                                return value.substring(1, pos).trim();
416                        } else if (quote == '(') { // list
417                                pos = value.indexOf(')', 1);
418                                return value.substring(1, pos).trim();
419                        } else { // number
420                                int posBlank = pos = value.indexOf(' ');
421                                int posTab   = value.indexOf('\t');
422                                if (posBlank == -1 && posTab != -1)
423                                        pos = posTab;
424                                else if (posBlank != -1 && posTab == -1)
425                                        pos = posBlank;
426                                else if (posBlank != -1 && posTab != -1)
427                                        pos = Math.min(posBlank, posTab);
428                                else
429                                        pos = value.length();
430                                return value.substring(0, pos).trim();
431                        }
432                }
433        }
434
435        /**
436         * Remove the comments from SQL statement, if exists.
437         * Multiline line (block) comments (start: «/*», stop: «*​/»)
438         * Single line comments    (start: «--», stop: «\n»)
439         *
440         * @param   sql a SQL string with comments.
441         * @return  a SQL string without comments
442         */
443        static String stripComments(String sql) {
444                sql = stripBlockComments(sql); // Strip block comments (start: «/*» stop: «*​/»)
445                sql = stripLineComments(sql);  // Strip line comments (start: «--», stop: «\n»)
446                return sql;
447        }
448
449        /**
450         * Remove the comments from SQL statement, if exists.
451         * Multiline line (block) comments (start: «/*», stop: «*​/»)
452         * Single line comments    (start: «--», stop: «\n»)
453         *
454         * @param   sql a SQL string with comments.
455         * @return  a SQL string without comments
456         */
457        private static String stripLineComments(String sql) {
458                // Strip line comments (start: «--», stop: «\n»)
459                while (true) {
460                        int offset = indexOf(sql, SINGLE_LINE_COMMENT_START);
461                        if (offset == -1)
462                                break;
463                        int pos2 = sql.indexOf(SINGLE_LINE_COMMENT_STOP, offset);
464                        if (pos2 == -1)
465                                sql = sql.substring(0, offset);
466                        else
467                                sql = sql.substring(0, offset) + sql.substring(pos2);
468                }
469                return sql.trim();
470        }
471
472        /**
473         * Remove the comments from SQL statement, if exists.
474         * Multiline line (block) comments (start: «/*», stop: «*​/»)
475         * Single line comments    (start: «--», stop: «\n»)
476         *
477         * @param   sql a SQL string with comments.
478         * @return  a SQL string without comments
479         */
480        private static String stripBlockComments(String sql) {
481                // Strip block comments (start: «/*» stop: «*​/»)
482                while (true) {
483                        int offset = indexOf(sql, MULTI_LINE_COMMENT_START);
484                        if (offset == -1)
485                                break;
486                        int pos2 = sql.indexOf(MULTI_LINE_COMMENT_STOP, offset);
487                        sql = sql.substring(0, offset) + sql.substring(pos2 + 2);
488                }
489                return sql.trim();
490        }
491
492        /**
493         * Process SQL statement
494         *
495         * @param sql - SQL statement
496         * @return    - processed SQL statement
497         */
498        public static String processStatement(String sql) {
499                sql = sql.trim();
500                while (sql.endsWith(STATEMENT_SEPARATOR)) // remove trailing separator(s)
501                        sql = sql.substring(0, sql.length() - 1).trim();
502                sql = stripComments(sql).trim();
503                StringBuilder sb    = new StringBuilder();
504                String[]      lines = sql.split("\n");
505                for (int i = 0; i < lines.length; i++)
506                        sb.append(' ' + lines[i].trim() + '\n');
507                return sb.toString().trim();
508        }
509
510        /**
511         * Parse named prepared statement and return parameter name to index map and
512         * replace host variables with '?' character to provide standard prepared statement form
513         * 
514         * @param sql       - named prepared statement
515         * @param resultSQL - standard prepared statement
516         * @return            map parameter name to index (indexes starts from 1) 
517         */
518        public static Map<String /* name */, List<Integer /* index */>> parseNamedPreparedStatement(String sql, StringBuilder resultSQL) {
519                Map<String /* name */, List<Integer /* index */>> map = new LinkedHashMap<>();
520
521                ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
522                resultSQL.append(NamedParameterUtils.parseSqlStatementIntoString(sql));
523
524                MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource();
525                List<SqlParameter>    params                = NamedParameterUtils.buildSqlParameterList(psql, mapSqlParameterSource);
526                //              
527                for (int i = 0; i < params.size(); i++) {
528                        String name = params.get(i).getName();
529
530                        List<Integer /* index */> indexes = map.get(name);
531                        if (indexes == null) {
532                                indexes = new ArrayList<>();
533                                map.put(name, indexes);
534                        }
535                        indexes.add(i + 1);
536                }
537                return map;
538        }
539
540        /**
541         * Parse SQL script by splitting it on SQL statements
542         * Standard semicolon character «;» is used as statement separator.
543         *
544         * This method also:
545         * - compress row(s) of each SQL statement by removing
546         * ambiguous  trailing white spaces characters.
547         * - remove trailing statement separator «;»
548         *
549         * @param sqlScript - SQL script to parse
550         * @param list      - list of all presented in script SQL statements (output parameter)
551         * @throws IOException 
552         */
553        private static void readSqlScriptToList(String sqlScript, List<String /* SQL statements */> list) throws IOException {
554                try (ScriptReader scriptReader = new ScriptReader(new StringReader(sqlScript))) {
555                        while (true) {
556                                String stat = scriptReader.readStatement();
557                                if (stat == null)
558                                        break;
559                                stat = stat.trim();
560                                stat = stripComments(stat).trim();
561                                if (!stat.isEmpty())
562                                        list.add(stat);
563                        }
564                }
565
566        }
567
568        /**
569         * Recursive method to process "INCLUDE SCRIPT FILE" statement
570         *
571         * @param path       - path to SQL script to parse
572         * @param list       - list of all presented in script SQL statements with injected includes (output parameter)
573         * @throws IOException
574         */
575        private static void processIncludes(Path path, List<String /* SQL statements */> list) throws IOException {
576                List<String /* SQL statements */> singleFileList = new ArrayList<>();
577                readSqlScriptToList(StringUtils.readAsText(path), singleFileList);
578
579                for (String statement : singleFileList) {
580                        String statementUpperCase = statement.toUpperCase(Locale.ENGLISH);
581                        if (statementUpperCase.startsWith(SPECIAL_STATEMENT_INCLUDE)) {
582                                List<String> fileNames = new ParseStmtInclude().parse(statement).fileNames;
583                                for (String fileName : fileNames) {
584                                        if (fileName.startsWith("/"))
585                                                path = Paths.get(fileName);
586                                        else
587                                                path = Paths.get(path.getParent().toString(), fileName);
588                                        if (Files.exists(path))
589                                                processIncludes(path, list); // recursive call
590                                }
591                        } else
592                                list.add(statement);
593                }
594        }
595
596        /**
597         * Read 'init.sql' file record by record and divide it by instance name to map
598         *
599         * @param path
600         * @param map
601         * @throws IOException
602         */
603        private static void separateSqlFile(Path path, List<String /* SQL statements */> list, Map<String /* connection name */, List<String /* SQL statements */>> map, Map<String /* connection name */, String /* parent directory */> parentDirectoryMap) throws IOException {
604                String       instanceName = null;
605                List<String> listBuffer   = null;
606                for (String statement : list) {
607                        String canonizedStatement = canonizeSql(statement);
608                        if (canonizedStatement.startsWith(SPECIAL_STATEMENT_CONNECT_TO)) {
609                                StmtConnectTo stmtConnectTo = new ParseStmtConnectTo().parse(null, statement);
610                                instanceName = stmtConnectTo.instanceName;
611                                listBuffer   = new ArrayList<>();
612                                listBuffer.add(statement);
613                                map.put(instanceName, listBuffer);
614                                parentDirectoryMap.put(instanceName, path.getParent().toString());
615                        } else if (canonizedStatement.startsWith(SPECIAL_STATEMENT_SWITCH_TO)) {
616                                StmtSwitchTo stmtSwitchTo = new ParseStmtSwitchTo().parse(null, statement);
617                                instanceName = stmtSwitchTo.instanceName;
618                                listBuffer   = map.get(instanceName);
619                                if (listBuffer == null)
620                                        throw new Error("CONNECT TO required prior " + statement);
621                        } else {
622                                if (listBuffer == null)
623                                        throw new Error("CONNECT TO required prior " + statement);
624                                listBuffer.add(statement);
625                        }
626                }
627        }
628
629        /**
630         * Iterate directory recursive, find 'init.sql' files, and process them.
631         *
632         * @param path
633         * @param sqlStatementsMap
634         * @throws IOException
635         */
636        public static void processInitSqlFiles(File file, Map<String /* connection name */, List<String /* SQL statements */>> sqlStatementsMap, Map<String /* connection name */, String /* parent directory */> parentDirectoryMap) throws IOException {
637                if (file.isDirectory()) {
638                        File[] files = file.listFiles();
639                        if (files != null)
640                                for (File f : files)
641                                        processInitSqlFiles(f, sqlStatementsMap, parentDirectoryMap);
642                } else {
643                        String fileName = file.getName();
644                        if (fileName.equals(INIT_SQL_FILE_EXT) || fileName.endsWith('.' + INIT_SQL_FILE_EXT))
645                                processInitSqlFile(file.toPath(), sqlStatementsMap, parentDirectoryMap);
646                }
647        }
648
649        /**
650         * Read 'init.sql' file, process 'includes', iterate record by record and divide it by instance name to map
651         *
652         * @param path
653         * @param sqlStatementsMap
654         * @throws IOException
655         */
656        private static void processInitSqlFile(Path path, Map<String /* connection name */, List<String /* SQL statements */>> sqlStatementsMap, Map<String /* connection name */, String /* parent directory */> parentDirectoryMap) throws IOException {
657                List<String /* SQL statements */> list = new ArrayList<>();
658                processIncludes(path, list);
659                separateSqlFile(path, list, sqlStatementsMap, parentDirectoryMap);
660        }
661
662        /**
663         * Canonize SQL statement for compare (startsWith)
664         * 
665         * E.g. "connect to" - > "CONNECTTO"
666         * @param sql
667         * @return
668         */
669        public static String canonizeSql(String sql) {
670                sql = stripComments(sql).trim();
671                sql = sql.replace("\n", "");
672                sql = sql.replace("\r", "");
673                sql = sql.replace("\t", "");
674                sql = sql.replace(" ", "");
675                sql = sql.toUpperCase(Locale.ENGLISH);
676                return sql;
677        }
678}
679
680/*
681Please contact FBSQL Team by E-Mail fbsql.team@gmail.com
682or visit https://fbsql.github.io if you need additional
683information or have any questions.
684*/
685
686/* EOF */