All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.impossibl.postgres.jdbc.SQLText Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (c) 2013, impossibl.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of impossibl.com nor the names of its contributors may
 *    be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package com.impossibl.postgres.jdbc;

import com.impossibl.postgres.jdbc.SQLTextTree.CommentPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.CompositeNode;
import com.impossibl.postgres.jdbc.SQLTextTree.EscapeNode;
import com.impossibl.postgres.jdbc.SQLTextTree.GrammarPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.MultiStatementNode;
import com.impossibl.postgres.jdbc.SQLTextTree.NumericLiteralPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.ParameterPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.ParenGroupNode;
import com.impossibl.postgres.jdbc.SQLTextTree.Processor;
import com.impossibl.postgres.jdbc.SQLTextTree.QuotedIdentifierPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.StatementNode;
import com.impossibl.postgres.jdbc.SQLTextTree.StringLiteralPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.UnquotedIdentifierPiece;
import com.impossibl.postgres.jdbc.SQLTextTree.WhitespacePiece;

import java.sql.SQLException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SQLText {

  private static final Set ESCAPE_STRING_SPECIFIERS = new HashSet<>(Arrays.asList('E', 'e'));

  private MultiStatementNode root;

  public SQLText(String sqlText) throws ParseException {
    this(sqlText, true);
  }

  public SQLText(String sqlText, boolean standardConformingStrings) throws ParseException {
    root = parse(sqlText, standardConformingStrings);
  }

  public SQLText(MultiStatementNode copyRoot) {
    root = copyRoot;
  }

  public SQLText copy() {
    return new SQLText((MultiStatementNode) root.copy());
  }

  public int getStatementCount() {
    if (root == null)
      return 0;
    return root.getNodeCount();
  }

  public StatementNode getFirstStatement() {
    if (root == null || root.getNodeCount() == 0)
      return null;
    return (StatementNode) root.get(0);
  }

  public StatementNode getLastStatement() {
    if (root == null || root.getNodeCount() == 0)
      return null;
    return (StatementNode) root.get(root.getNodeCount() - 1);
  }

  public void addStatements(SQLText sqlText) {
    root.nodes.addAll(sqlText.root.nodes);
  }

  public void process(Processor processor, boolean recurse) throws SQLException {
    root.process(processor, recurse);
  }

  @Override
  public String toString() {
    return root.toString();
  }

  public static MultiStatementNode parse(final String sql, boolean standardConformingStrings) throws ParseException {

    Deque parents = new LinkedList<>();

    parents.push(new MultiStatementNode(0));
    parents.push(new StatementNode(0));

    int paramId = 1;
    int ndx = 0;

    try {
      while (ndx < sql.length()) {

        char c = sql.charAt(ndx);
        switch (c) {
          case '\'':
            ndx = consumeStringLiteral(sql, ndx + 1, parents.peek(), standardConformingStrings);
            continue;
          case '"':
            ndx = consumeQuotedIdentifier(sql, ndx, parents.peek());
            continue;
          case '?':
            char nextChar = lookAhead(sql, ndx);
            if (nextChar == '|' || nextChar == '&' || nextChar == '?') {
              final GrammarPiece grammarPiece;
              if (nextChar == '?') {
                grammarPiece = new GrammarPiece("?", ndx);
              }
              else {
                grammarPiece = new GrammarPiece("?" + nextChar, ndx);
              }
              parents.peek().add(grammarPiece);
              ++ndx;
            }
            else {
              ParameterPiece parameterPiece = new ParameterPiece(paramId++, ndx);
              parents.peek().add(parameterPiece);
            }
            break;
          case '$':
            ndx = consumeDollar(sql, ndx, parents.peek());
            continue;
          case '(':
          case ')':
            ndx = consumeParens(sql, ndx, parents);
            continue;
          case '{':
          case '}':
            ndx = consumeBraces(sql, ndx, parents);
            continue;
          case '/':
            if (lookAhead(sql, ndx) == '*') {
              ndx = consumeMultilineComment(sql, ndx, parents.peek());
              continue;
            }
            else {
              parents.peek().add(new GrammarPiece("/", ndx));
              break;
            }
          case '-':
            if (lookAhead(sql, ndx) == '-') {
              ndx = consumeSinglelineComment(sql, ndx, parents.peek());
              continue;
            }
            else if (Character.isDigit(lookAhead(sql, ndx))) {
              ndx = consumeNumeric(sql, ndx, parents.peek());
              continue;
            }
            else {
              GrammarPiece grammarPiece = new GrammarPiece("-", ndx);
              parents.peek().add(grammarPiece);
              break;
            }
          case ';':
            if (parents.size() == 2) {
              paramId = 1;
              CompositeNode comp = parents.pop();
              comp.setEndPos(ndx);
              parents.peek().add(comp);
              parents.push(new StatementNode(ndx));
            }
            else {
              parents.peek().add(new GrammarPiece(";", ndx));
            }
            break;
          default:
            if (Character.isWhitespace(c)) {
              WhitespacePiece whitespacePiece = new WhitespacePiece(sql.substring(ndx, ndx + 1), ndx);
              if (parents.peek().getLastNode() instanceof WhitespacePiece) {
                ((WhitespacePiece) parents.peek().getLastNode()).coalesce(whitespacePiece);
              }
              else {
                parents.peek().add(whitespacePiece);
              }
            }
            else if (Character.isDigit(c) || (c == '+' && Character.isDigit(lookAhead(sql, ndx)))) {
              ndx = consumeNumeric(sql, ndx, parents.peek());
              continue;
            }
            else if (Character.isJavaIdentifierStart(c)) {
              ndx = consumeUnquotedIdentifier(sql, ndx, parents.peek());
              continue;
            }
            else {
              GrammarPiece grammarPiece = new GrammarPiece(sql.substring(ndx, ndx + 1), ndx);
              if (parents.peek().getLastNode() instanceof GrammarPiece) {
                ((GrammarPiece) parents.peek().getLastNode()).coalesce(grammarPiece);
              }
              else {
                parents.peek().add(grammarPiece);
              }
            }
        }

        ++ndx;
      }

      // Auto close last statement
      if (parents.peek() instanceof StatementNode) {

        StatementNode stmt = (StatementNode) parents.peek();

        stmt.trim();

        if (stmt.getNodeCount() > 0) {
          CompositeNode tmp = parents.pop();
          tmp.setEndPos(ndx);
          parents.peek().add(tmp);
        }
      }

      if (!(parents.peek() instanceof StatementNode) && !(parents.peek() instanceof MultiStatementNode)) {
        throw new IllegalArgumentException("error parsing SQL");
      }

      return (MultiStatementNode) parents.getLast();
    }
    catch (ParseException e) {
      throw e;
    }
    catch (Exception e) {
      String errorTxt = sql.substring(ndx, Math.min(sql.length(), ndx + 10));
      throw new ParseException("Error near: " + errorTxt, ndx);
    }
  }

  private static int consumeNumeric(final String sql, final int start, final CompositeNode parent) throws ParseException {
    Matcher matcher = Pattern.compile("((?:[+-]?(?:\\d+)?(?:\\.\\d+(?:[eE][+-]?\\d+)?))|(?:[+-]?\\d+))").matcher(sql.substring(start));
    if (matcher.find()) {
      parent.add(new NumericLiteralPiece(matcher.group(1), matcher.start()));
      return start + matcher.group(1).length();
    }
    else {
      throw new ParseException("Invalid numeric literal", start);
    }
  }

  private static int consumeUnquotedIdentifier(final String sql, final int start, final CompositeNode parent) {
    int ndx = start;
    char c;
    do {
      c = lookAhead(sql, ndx++);
    }
    while (ndx < sql.length() && Character.isJavaIdentifierPart(c));

    parent.add(new UnquotedIdentifierPiece(sql.substring(start, ndx), start));

    return ndx;
  }

  private static int consumeBraces(final String sql, final int start, final Deque parents) throws ParseException {
    if (sql.charAt(start) == '{') {
      parents.push(new EscapeNode(start));
    }
    else {
      if (parents.peek() instanceof EscapeNode) {
        EscapeNode tmp = (EscapeNode) parents.pop();
        tmp.setEndPos(start);
        parents.peek().add(tmp);
      }
      else {
        throw new ParseException("Mismatched curly brace", start);
      }
    }

    return start + 1;
  }

  private static int consumeParens(final String sql, final int start, final Deque parents) throws ParseException {
    if (sql.charAt(start) == '(') {
      parents.push(new ParenGroupNode(start));
    }
    else {
      if (parents.peek() instanceof ParenGroupNode) {
        ParenGroupNode tmp = (ParenGroupNode) parents.pop();
        tmp.setEndPos(start);
        parents.peek().add(tmp);
      }
      else {
        throw new ParseException("Mismmatched parenthesis", start);
      }
    }

    return start + 1;
  }

  private static int consumeDollar(final String sql, final int start, final CompositeNode parent) throws ParseException {
    int ndx = start;
    do {
      if (lookAhead(sql, ndx) == '$') {
        final String ident = sql.substring(start, ndx + 2);
        final int pos = sql.indexOf(ident, ndx + 2);
        if (pos != -1) {
          String quotedText = sql.substring(ndx + 2, pos);
          parent.add(new StringLiteralPiece(quotedText, ident, start));
          return pos + ident.length();
        }
        else {
          ++ndx;
          break;
        }
      }
    } while (++ndx < sql.length());

    // Just treat as a grammar piece
    parent.add(new GrammarPiece(sql.substring(start, ndx), start));
    return ndx;
  }

  private static int consumeStringLiteral(final String sql, final int start, final CompositeNode parent, final boolean standardConformingStrings) throws ParseException {
    final char charBeforeLiteral = sql.charAt(start - 2);
    final boolean standard = standardConformingStrings && !ESCAPE_STRING_SPECIFIERS.contains(charBeforeLiteral);

    int ndx = start;
    do {
      char c = sql.charAt(ndx);
      if (c == '\'') {
        if (!standard && sql.charAt(ndx - 1) == '\\') {  // look-behind
          // skip escaped
        }
        else {
          break;
        }
      }

      if (lookAhead(sql, ndx) == 0) {
        throw new ParseException("Unterminated string literal", start);
      }

      ++ndx;
    } while (true);

    StringLiteralPiece literalPiece = new StringLiteralPiece(sql.substring(start, ndx), start);
    parent.add(literalPiece);

    return ndx + 1;
  }

  private static int consumeQuotedIdentifier(final String sql, final int start, final CompositeNode parent) throws ParseException {
    int ndx = start + 1, adjacentQuoteCount = 0;
    do {
      char c = sql.charAt(ndx);
      if (c == '"') {
        ++adjacentQuoteCount;
        if (sql.charAt(ndx - 1) == '"' && (adjacentQuoteCount % 2) == 0) {  // look-behind
          // skip escaped
        }
        else {
          if ((ndx == sql.length() - 1 || sql.charAt(ndx + 1) != '"')) {  // look-ahead
            break;
          }
        }
      }
      else {
        adjacentQuoteCount = 0;
      }

      if (lookAhead(sql, ndx) == 0) {
        throw new ParseException("Unterminated string literal", start);
      }

      ++ndx;
    } while (true);

    QuotedIdentifierPiece literalPiece = new QuotedIdentifierPiece(sql.substring(start + 1, ndx), start);
    parent.add(literalPiece);

    return ndx + 1;
  }

  private static int consumeSinglelineComment(final String sql, final int start, final CompositeNode parent) {
    int ndx = start + 2;

    if (ndx < sql.length()) {
      do {
        char c = sql.charAt(ndx);
        if (c == '\r' || c == '\n') {
          ndx = (lookAhead(sql, ndx) == '\n') ? ndx + 2 : ndx + 1;
          break;
        }
      } while (++ndx < sql.length());
    }
    else {
      ndx = sql.length();
    }

    CommentPiece commentPiece = new CommentPiece(sql.substring(start, ndx), start);
    parent.add(commentPiece);

    return ndx;
  }

  private static int consumeMultilineComment(final String sql, final int start, final CompositeNode parent) throws ParseException {
    int nestLevel = 1;
    int ndx = start + 1;
    do {
      char c = lookAhead(sql, ndx);
      if (c == 0) {
        throw new ParseException("Unterminated comment", start);
      }

      if (c == '/' && lookAhead(sql, ndx + 1) == '*') {
        ++nestLevel;
        ++ndx;
      }
      else if (c == '*' && lookAhead(sql, ndx + 1) == '/') {
        --nestLevel;
        ++ndx;
      }

      ++ndx;
    } while (nestLevel > 0);

    CommentPiece commentPiece = new CommentPiece(sql.substring(start, ndx + 1), start);
    parent.add(commentPiece);

    return ndx + 1;
  }

  private static char lookAhead(final String sql, final int ndx) {
    if (ndx + 1 < sql.length()) {
      return sql.charAt(ndx + 1);
    }

    return 0;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy