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 */