content: /*
   *
   *  *    Copyright (C) 2019 Amit Shekhar
   *  *    Copyright (C) 2011 Android Open Source Project
   *  *
   *  *    Licensed under the Apache License, Version 2.0 (the "License");
   *  *    you may not use this file except in compliance with the License.
   *  *    You may obtain a copy of the License at
   *  *
   *  *        http://www.apache.org/licenses/LICENSE-2.0
   *  *
   *  *    Unless required by applicable law or agreed to in writing, software
   *  *    distributed under the License is distributed on an "AS IS" BASIS,
   *  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   *  *    See the License for the specific language governing permissions and
   *  *    limitations under the License.
   *
   */
  package com.amitshekhar.utils;
  
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Ultra light, Ultra fast parser to extract table name out SQLs, supports oracle dialect SQLs as well.
   *
   * @author Nadeem Mohammad
   *         <p>
   *         Ref : https://github.com/mnadeem/sql-table-name-parser
   */
  public final class TableNameParser {
  
      private static final int NO_INDEX = -1;
      private static final String SPACE = " ";
      private static final String REGEX_SPACE = "\\s+";
  
      private static final String TOKEN_ORACLE_HINT_START = "/*+";
      private static final String TOKEN_ORACLE_HINT_END = "*/";
      private static final String TOKEN_SINGLE_LINE_COMMENT = "--";
      private static final String TOKEN_SEMI_COLON = ";";
      private static final String TOKEN_PARAN_START = "(";
      private static final String TOKEN_COMMA = ",";
      private static final String TOKEN_SET = "set";
      private static final String TOKEN_OF = "of";
      private static final String TOKEN_DUAL = "dual";
      private static final String TOKEN_DELETE = "delete";
      private static final String TOKEN_CREATE = "create";
      private static final String TOKEN_INDEX = "index";
      private static final String TOKEN_ASTERICK = "*";
      private static final String KEYWORD_JOIN = "join";
      private static final String KEYWORD_INTO = "into";
      private static final String KEYWORD_TABLE = "table";
      private static final String KEYWORD_FROM = "from";
      private static final String KEYWORD_USING = "using";
      private static final String KEYWORD_UPDATE = "update";
      private static final List<String> concerned = Arrays.asList(KEYWORD_TABLE, KEYWORD_INTO, KEYWORD_JOIN, KEYWORD_USING, KEYWORD_UPDATE);
      private static final List<String> ignored = Arrays.asList(TOKEN_PARAN_START, TOKEN_SET, TOKEN_OF, TOKEN_DUAL);
      private static String TOKEN_NEWLINE = "\\r\\n|\\r|\\n|\\n\\r";
      private Map<String, String> tables = new HashMap<String, String>();
  
      /**
       * Extracts table names out of SQL
       *
       * @param sql
       */
      public TableNameParser(final String sql) {
          String nocomments = removeComments(sql);
          String normalized = normalized(nocomments);
          String cleansed = clean(normalized);
          String[] tokens = cleansed.split(REGEX_SPACE);
          int index = 0;
  
          String firstToken = tokens[index];
          if (isOracleSpecialDelete(firstToken, tokens, index)) {
              handleSpecialOracleSpecialDelete(firstToken, tokens, index);
          } else if (isCreateIndex(firstToken, tokens, index)) {
              handleCreateIndex(firstToken, tokens, index);
          } else {
              while (moreTokens(tokens, index)) {
                  String currentToken = tokens[index++];
  
                  if (isFromToken(currentToken)) {
                      processFromToken(tokens, index);
                  } else if (shouldProcess(currentToken)) {
                      String nextToken = tokens[index++];
                      considerInclusion(nextToken);
  
                      if (moreTokens(tokens, index)) {
                          nextToken = tokens[index++];
                      }
                  }
              }
          }
      }
  
      private String removeComments(final String sql) {
          StringBuilder sb = new StringBuilder(sql);
          int nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT);
          while (nextCommentPosition > -1) {
              int end = indexOfRegex(TOKEN_NEWLINE, sb.substring(nextCommentPosition));
              if (end == -1) {
                  return sb.substring(0, nextCommentPosition);
              } else {
                  sb.replace(nextCommentPosition, end + nextCommentPosition, "");
              }
              nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT);
          }
          return sb.toString();
      }
  
      private int indexOfRegex(String regex, String string) {
          Pattern pattern = Pattern.compile(regex);
          Matcher matcher = pattern.matcher(string);
          return matcher.find() ? matcher.start() : -1;
      }
  
      private String normalized(final String sql) {
          String normalized = sql.trim().replaceAll(TOKEN_NEWLINE, SPACE).replaceAll(TOKEN_COMMA, " , ")
                  .replaceAll("\\(", " ( ").replaceAll("\\)", " ) ");
          if (normalized.endsWith(TOKEN_SEMI_COLON)) {
              normalized = normalized.substring(0, normalized.length() - 1);
          }
          return normalized;
      }
  
      private String clean(final String normalized) {
          int start = normalized.indexOf(TOKEN_ORACLE_HINT_START);
          int end = NO_INDEX;
          if (start != NO_INDEX) {
              end = normalized.indexOf(TOKEN_ORACLE_HINT_END);
              if (end != NO_INDEX) {
                  String firstHalf = normalized.substring(0, start);
                  String secondHalf = normalized.substring(end + 2, normalized.length());
                  return firstHalf.trim() + SPACE + secondHalf.trim();
              }
          }
          return normalized;
      }
  
      private boolean isOracleSpecialDelete(final String currentToken, final String[] tokens, int index) {
          index++;// Point to next token
          if (TOKEN_DELETE.equals(currentToken)) {
              if (moreTokens(tokens, index)) {
                  String nextToken = tokens[index++];
                  if (!KEYWORD_FROM.equals(nextToken) && !TOKEN_ASTERICK.equals(nextToken)) {
                      return true;
                  }
              }
          }
          return false;
      }
  
      private void handleSpecialOracleSpecialDelete(final String currentToken, final String[] tokens, int index) {
          String tableName = tokens[index + 1];
          considerInclusion(tableName);
      }
  
      private boolean isCreateIndex(String currentToken, String[] tokens, int index) {
          index++; // Point to next token
          if (TOKEN_CREATE.equals(currentToken.toLowerCase()) && hasIthToken(tokens, index, 3)) {
              String nextToken = tokens[index++];
              if (TOKEN_INDEX.equals(nextToken.toLowerCase())) {
                  return true;
              }
  
          }
          return false;
      }
  
      private void handleCreateIndex(String currentToken, String[] tokens, int index) {
          String tableName = tokens[index + 4];
          considerInclusion(tableName);
      }
  
      private boolean hasIthToken(String[] tokens, int currentIndex, int tokenNumber) {
          if (moreTokens(tokens, currentIndex) && tokens.length > currentIndex + tokenNumber) {
              return true;
          }
          return false;
      }
  
      private boolean shouldProcess(final String currentToken) {
          return concerned.contains(currentToken.toLowerCase());
      }
  
      private boolean isFromToken(final String currentToken) {
          return KEYWORD_FROM.equals(currentToken.toLowerCase());
      }
  
      private void processFromToken(final String[] tokens, int index) {
          String currentToken = tokens[index++];
          considerInclusion(currentToken);
  
          String nextToken = null;
          if (moreTokens(tokens, index)) {
              nextToken = tokens[index++];
          }
  
          if (shouldProcessMultipleTables(nextToken)) {
              processNonAliasedMultiTables(tokens, index, nextToken);
          } else {
              processAliasedMultiTables(tokens, index, currentToken);
          }
      }
  
      private void processNonAliasedMultiTables(final String[] tokens, int index, String nextToken) {
          while (nextToken.equals(TOKEN_COMMA)) {
              String currentToken = tokens[index++];
              considerInclusion(currentToken);
              if (moreTokens(tokens, index)) {
                  nextToken = tokens[index++];
              } else {
                  break;
              }
          }
      }
  
      private void processAliasedMultiTables(final String[] tokens, int index, String currentToken) {
          String nextNextToken = null;
          if (moreTokens(tokens, index)) {
              nextNextToken = tokens[index++];
          }
  
          if (shouldProcessMultipleTables(nextNextToken)) {
              while (moreTokens(tokens, index) && nextNextToken.equals(TOKEN_COMMA)) {
                  if (moreTokens(tokens, index)) {
                      currentToken = tokens[index++];
                  }
                  if (moreTokens(tokens, index)) {
                      index++;
                  }
                  if (moreTokens(tokens, index)) {
                      nextNextToken = tokens[index++];
                  }
                  considerInclusion(currentToken);
              }
          }
      }
  
      private boolean shouldProcessMultipleTables(final String nextToken) {
          return nextToken != null && nextToken.equals(TOKEN_COMMA);
      }
  
      private boolean moreTokens(final String[] tokens, int index) {
          return index < tokens.length;
      }
  
      private void considerInclusion(final String token) {
          if (!ignored.contains(token.toLowerCase()) && !this.tables.containsKey(token.toLowerCase())) {
              this.tables.put(token.toLowerCase(), token);
          }
      }
  
      /**
       * @return table names extracted out of sql
       */
      public Collection<String> tables() {
          return new HashSet<String>(this.tables.values());
      }
  }
  