content: /*
   * Copyright 1999-2012 Alibaba Group.
   *  
   * 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.
   */
  /**
   * (created at 2011-5-10)
   */
  package com.alibaba.cobar.parser.recognizer.mysql.syntax;
  
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.IDENTIFIER;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_AS;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_BY;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_FOR;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_GROUP;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_JOIN;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_ORDER;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_OUTER;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_UNION;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.KW_WITH;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.LITERAL_CHARS;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.PUNC_COMMA;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.PUNC_LEFT_PAREN;
  import static com.alibaba.cobar.parser.recognizer.mysql.MySQLToken.PUNC_RIGHT_PAREN;
  
  import java.sql.SQLSyntaxErrorException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.LinkedList;
  import java.util.List;
  
  import com.alibaba.cobar.parser.ast.expression.Expression;
  import com.alibaba.cobar.parser.ast.expression.misc.QueryExpression;
  import com.alibaba.cobar.parser.ast.expression.primary.Identifier;
  import com.alibaba.cobar.parser.ast.fragment.GroupBy;
  import com.alibaba.cobar.parser.ast.fragment.OrderBy;
  import com.alibaba.cobar.parser.ast.fragment.SortOrder;
  import com.alibaba.cobar.parser.ast.fragment.tableref.IndexHint;
  import com.alibaba.cobar.parser.ast.fragment.tableref.InnerJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.NaturalJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.OuterJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.StraightJoin;
  import com.alibaba.cobar.parser.ast.fragment.tableref.SubqueryFactor;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableRefFactor;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableReference;
  import com.alibaba.cobar.parser.ast.fragment.tableref.TableReferences;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLQueryStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLSelectStatement;
  import com.alibaba.cobar.parser.ast.stmt.dml.DMLSelectUnionStatement;
  import com.alibaba.cobar.parser.recognizer.mysql.MySQLToken;
  import com.alibaba.cobar.parser.recognizer.mysql.lexer.MySQLLexer;
  
  /**
   * @author <a href="mailto:shuo.qius@alibaba-inc.com">QIU Shuo</a>
   */
  public abstract class MySQLDMLParser extends MySQLParser {
      protected MySQLExprParser exprParser;
  
      public MySQLDMLParser(MySQLLexer lexer, MySQLExprParser exprParser) {
          super(lexer);
          this.exprParser = exprParser;
      }
  
      /**
       * nothing has been pre-consumed
       * 
       * @return null if there is no order by
       */
      protected GroupBy groupBy() throws SQLSyntaxErrorException {
          if (lexer.token() != KW_GROUP) {
              return null;
          }
          lexer.nextToken();
          match(KW_BY);
          Expression expr = exprParser.expression();
          SortOrder order = SortOrder.ASC;
          GroupBy groupBy;
          switch (lexer.token()) {
          case KW_DESC:
              order = SortOrder.DESC;
          case KW_ASC:
              lexer.nextToken();
          default:
              break;
          }
          switch (lexer.token()) {
          case KW_WITH:
              lexer.nextToken();
              matchIdentifier("ROLLUP");
              return new GroupBy(expr, order, true);
          case PUNC_COMMA:
              break;
          default:
              return new GroupBy(expr, order, false);
          }
          for (groupBy = new GroupBy().addOrderByItem(expr, order); lexer.token() == PUNC_COMMA;) {
              lexer.nextToken();
              order = SortOrder.ASC;
              expr = exprParser.expression();
              switch (lexer.token()) {
              case KW_DESC:
                  order = SortOrder.DESC;
              case KW_ASC:
                  lexer.nextToken();
              default:
                  break;
              }
              groupBy.addOrderByItem(expr, order);
              if (lexer.token() == KW_WITH) {
                  lexer.nextToken();
                  matchIdentifier("ROLLUP");
                  return groupBy.setWithRollup();
              }
          }
          return groupBy;
      }
  
      /**
       * nothing has been pre-consumed
       * 
       * @return null if there is no order by
       */
      protected OrderBy orderBy() throws SQLSyntaxErrorException {
          if (lexer.token() != KW_ORDER) {
              return null;
          }
          lexer.nextToken();
          match(KW_BY);
          Expression expr = exprParser.expression();
          SortOrder order = SortOrder.ASC;
          OrderBy orderBy;
          switch (lexer.token()) {
          case KW_DESC:
              order = SortOrder.DESC;
          case KW_ASC:
              if (lexer.nextToken() != PUNC_COMMA) {
                  return new OrderBy(expr, order);
              }
          case PUNC_COMMA:
              orderBy = new OrderBy();
              orderBy.addOrderByItem(expr, order);
              break;
          default:
              return new OrderBy(expr, order);
          }
          for (; lexer.token() == PUNC_COMMA;) {
              lexer.nextToken();
              order = SortOrder.ASC;
              expr = exprParser.expression();
              switch (lexer.token()) {
              case KW_DESC:
                  order = SortOrder.DESC;
              case KW_ASC:
                  lexer.nextToken();
              }
              orderBy.addOrderByItem(expr, order);
          }
          return orderBy;
      }
  
      /**
       * @param id never null
       */
      protected List<Identifier> buildIdList(Identifier id) throws SQLSyntaxErrorException {
          if (lexer.token() != PUNC_COMMA) {
              List<Identifier> list = new ArrayList<Identifier>(1);
              list.add(id);
              return list;
          }
          List<Identifier> list = new LinkedList<Identifier>();
          list.add(id);
          for (; lexer.token() == PUNC_COMMA;) {
              lexer.nextToken();
              id = identifier();
              list.add(id);
          }
          return list;
      }
  
      /**
       * <code>(id (',' id)*)?</code>
       * 
       * @return never null or empty. {@link LinkedList} is possible
       */
      protected List<Identifier> idList() throws SQLSyntaxErrorException {
          return buildIdList(identifier());
      }
  
      /**
       * <code>( idName (',' idName)*)? ')'</code>
       * 
       * @return empty list if emtpy id list
       */
      protected List<String> idNameList() throws SQLSyntaxErrorException {
          if (lexer.token() != IDENTIFIER) {
              match(PUNC_RIGHT_PAREN);
              return Collections.emptyList();
          }
          List<String> list;
          String str = lexer.stringValue();
          if (lexer.nextToken() == PUNC_COMMA) {
              list = new LinkedList<String>();
              list.add(str);
              for (; lexer.token() == PUNC_COMMA;) {
                  lexer.nextToken();
                  list.add(lexer.stringValue());
                  match(IDENTIFIER);
              }
          } else {
              list = new ArrayList<String>(1);
              list.add(str);
          }
          match(PUNC_RIGHT_PAREN);
          return list;
      }
  
      /**
       * @return never null
       */
      protected TableReferences tableRefs() throws SQLSyntaxErrorException {
          TableReference ref = tableReference();
          return buildTableReferences(ref);
      }
  
      private TableReferences buildTableReferences(TableReference ref) throws SQLSyntaxErrorException {
          List<TableReference> list;
          if (lexer.token() == PUNC_COMMA) {
              list = new LinkedList<TableReference>();
              list.add(ref);
              for (; lexer.token() == PUNC_COMMA;) {
                  lexer.nextToken();
                  ref = tableReference();
                  list.add(ref);
              }
          } else {
              list = new ArrayList<TableReference>(1);
              list.add(ref);
          }
          return new TableReferences(list);
      }
  
      private TableReference tableReference() throws SQLSyntaxErrorException {
          TableReference ref = tableFactor();
          return buildTableReference(ref);
      }
  
      @SuppressWarnings("unchecked")
      private TableReference buildTableReference(TableReference ref) throws SQLSyntaxErrorException {
          for (;;) {
              Expression on;
              List<String> using;
              TableReference temp;
              boolean isOut = false;
              boolean isLeft = true;
              switch (lexer.token()) {
              case KW_INNER:
              case KW_CROSS:
                  lexer.nextToken();
              case KW_JOIN:
                  lexer.nextToken();
                  temp = tableFactor();
                  switch (lexer.token()) {
                  case KW_ON:
                      lexer.nextToken();
                      on = exprParser.expression();
                      ref = new InnerJoin(ref, temp, on);
                      break;
                  case KW_USING:
                      lexer.nextToken();
                      match(PUNC_LEFT_PAREN);
                      using = idNameList();
                      ref = new InnerJoin(ref, temp, using);
                      break;
                  default:
                      ref = new InnerJoin(ref, temp);
                      break;
                  }
                  break;
              case KW_STRAIGHT_JOIN:
                  lexer.nextToken();
                  temp = tableFactor();
                  switch (lexer.token()) {
                  case KW_ON:
                      lexer.nextToken();
                      on = exprParser.expression();
                      ref = new StraightJoin(ref, temp, on);
                      break;
                  default:
                      ref = new StraightJoin(ref, temp);
                      break;
                  }
                  break;
              case KW_RIGHT:
                  isLeft = false;
              case KW_LEFT:
                  lexer.nextToken();
                  if (lexer.token() == KW_OUTER) {
                      lexer.nextToken();
                  }
                  match(KW_JOIN);
                  temp = tableReference();
                  switch (lexer.token()) {
                  case KW_ON:
                      lexer.nextToken();
                      on = exprParser.expression();
                      ref = new OuterJoin(isLeft, ref, temp, on);
                      break;
                  case KW_USING:
                      lexer.nextToken();
                      match(PUNC_LEFT_PAREN);
                      using = idNameList();
                      ref = new OuterJoin(isLeft, ref, temp, using);
                      break;
                  default:
                      Object condition = temp.removeLastConditionElement();
                      if (condition instanceof Expression) {
                          ref = new OuterJoin(isLeft, ref, temp, (Expression) condition);
                      } else if (condition instanceof List) {
                          ref = new OuterJoin(isLeft, ref, temp, (List<String>) condition);
                      } else {
                          throw err("conditionExpr cannot be null for outer join");
                      }
                      break;
                  }
                  break;
              case KW_NATURAL:
                  lexer.nextToken();
                  switch (lexer.token()) {
                  case KW_RIGHT:
                      isLeft = false;
                  case KW_LEFT:
                      lexer.nextToken();
                      if (lexer.token() == KW_OUTER) {
                          lexer.nextToken();
                      }
                      isOut = true;
                  case KW_JOIN:
                      lexer.nextToken();
                      temp = tableFactor();
                      ref = new NaturalJoin(isOut, isLeft, ref, temp);
                      break;
                  default:
                      throw err("unexpected token after NATURAL for natural join:" + lexer.token());
                  }
                  break;
              default:
                  return ref;
              }
          }
      }
  
      private TableReference tableFactor() throws SQLSyntaxErrorException {
          String alias = null;
          switch (lexer.token()) {
          case PUNC_LEFT_PAREN:
              lexer.nextToken();
              Object ref = trsOrQuery();
              match(PUNC_RIGHT_PAREN);
              if (ref instanceof QueryExpression) {
                  alias = as();
                  return new SubqueryFactor((QueryExpression) ref, alias);
              }
              return (TableReferences) ref;
          case IDENTIFIER:
              Identifier table = identifier();
              alias = as();
              List<IndexHint> hintList = hintList();
              return new TableRefFactor(table, alias, hintList);
          default:
              throw err("unexpected token for tableFactor: " + lexer.token());
          }
      }
  
      /**
       * @return never empty. upper-case if id format.
       *         <code>"alias1" |"`al`ias1`" | "'alias1'" | "_latin1'alias1'"</code>
       */
      protected String as() throws SQLSyntaxErrorException {
          if (lexer.token() == KW_AS) {
              lexer.nextToken();
          }
          StringBuilder alias = new StringBuilder();
          boolean id = false;
          if (lexer.token() == IDENTIFIER) {
              alias.append(lexer.stringValueUppercase());
              id = true;
              lexer.nextToken();
          }
          if (lexer.token() == LITERAL_CHARS) {
              if (!id || id && alias.charAt(0) == '_') {
                  alias.append(lexer.stringValue());
                  lexer.nextToken();
              }
          }
          return alias.length() > 0 ? alias.toString() : null;
      }
  
      /**
       * @return type of {@link QueryExpression} or {@link TableReferences}
       */
      private Object trsOrQuery() throws SQLSyntaxErrorException {
          Object ref;
          switch (lexer.token()) {
          case KW_SELECT:
              DMLSelectStatement select = selectPrimary();
              return buildUnionSelect(select);
          case PUNC_LEFT_PAREN:
              lexer.nextToken();
              ref = trsOrQuery();
              match(PUNC_RIGHT_PAREN);
              if (ref instanceof QueryExpression) {
                  if (ref instanceof DMLSelectStatement) {
                      QueryExpression rst = buildUnionSelect((DMLSelectStatement) ref);
                      if (rst != ref) {
                          return rst;
                      }
                  }
                  String alias = as();
                  if (alias != null) {
                      ref = new SubqueryFactor((QueryExpression) ref, alias);
                  } else {
                      return ref;
                  }
              }
              // ---- build factor complete---------------
              ref = buildTableReference((TableReference) ref);
              // ---- build ref complete---------------
              break;
          default:
              ref = tableReference();
          }
  
          List<TableReference> list;
          if (lexer.token() == PUNC_COMMA) {
              list = new LinkedList<TableReference>();
              list.add((TableReference) ref);
              for (; lexer.token() == PUNC_COMMA;) {
                  lexer.nextToken();
                  ref = tableReference();
                  list.add((TableReference) ref);
              }
              return new TableReferences(list);
          }
          list = new ArrayList<TableReference>(1);
          list.add((TableReference) ref);
          return new TableReferences(list);
      }
  
      /**
       * @return null if there is no hint
       */
      private List<IndexHint> hintList() throws SQLSyntaxErrorException {
          IndexHint hint = hint();
          if (hint == null)
              return null;
          List<IndexHint> list;
          IndexHint hint2 = hint();
          if (hint2 == null) {
              list = new ArrayList<IndexHint>(1);
              list.add(hint);
              return list;
          }
          list = new LinkedList<IndexHint>();
          list.add(hint);
          list.add(hint2);
          for (; (hint2 = hint()) != null; list.add(hint2));
          return list;
      }
  
      /**
       * @return null if there is no hint
       */
      private IndexHint hint() throws SQLSyntaxErrorException {
          IndexHint.IndexAction action;
          switch (lexer.token()) {
          case KW_USE:
              action = IndexHint.IndexAction.USE;
              break;
          case KW_IGNORE:
              action = IndexHint.IndexAction.IGNORE;
              break;
          case KW_FORCE:
              action = IndexHint.IndexAction.FORCE;
              break;
          default:
              return null;
          }
          IndexHint.IndexType type;
          switch (lexer.nextToken()) {
          case KW_INDEX:
              type = IndexHint.IndexType.INDEX;
              break;
          case KW_KEY:
              type = IndexHint.IndexType.KEY;
              break;
          default:
              throw err("must be INDEX or KEY for hint type, not " + lexer.token());
          }
          IndexHint.IndexScope scope = IndexHint.IndexScope.ALL;
          if (lexer.nextToken() == KW_FOR) {
              switch (lexer.nextToken()) {
              case KW_JOIN:
                  lexer.nextToken();
                  scope = IndexHint.IndexScope.JOIN;
                  break;
              case KW_ORDER:
                  lexer.nextToken();
                  match(KW_BY);
                  scope = IndexHint.IndexScope.ORDER_BY;
                  break;
              case KW_GROUP:
                  lexer.nextToken();
                  match(KW_BY);
                  scope = IndexHint.IndexScope.GROUP_BY;
                  break;
              default:
                  throw err("must be JOIN or ORDER or GROUP for hint scope, not " + lexer.token());
              }
          }
  
          match(PUNC_LEFT_PAREN);
          List<String> indexList = idNameList();
          return new IndexHint(action, type, scope, indexList);
      }
  
      /**
       * @return argument itself if there is no union
       */
      protected DMLQueryStatement buildUnionSelect(DMLSelectStatement select) throws SQLSyntaxErrorException {
          if (lexer.token() != KW_UNION) {
              return select;
          }
          DMLSelectUnionStatement union = new DMLSelectUnionStatement(select);
          for (; lexer.token() == KW_UNION;) {
              lexer.nextToken();
              boolean isAll = false;
              switch (lexer.token()) {
              case KW_ALL:
                  isAll = true;
              case KW_DISTINCT:
                  lexer.nextToken();
                  break;
              }
              select = selectPrimary();
              union.addSelect(select, isAll);
          }
          union.setOrderBy(orderBy()).setLimit(limit());
          return union;
      }
  
      protected DMLSelectStatement selectPrimary() throws SQLSyntaxErrorException {
          switch (lexer.token()) {
          case KW_SELECT:
              return select();
          case PUNC_LEFT_PAREN:
              lexer.nextToken();
              DMLSelectStatement select = selectPrimary();
              match(PUNC_RIGHT_PAREN);
              return select;
          default:
              throw err("unexpected token: " + lexer.token());
          }
      }
  
      /**
       * first token is {@link MySQLToken#KW_SELECT SELECT} which has been scanned
       * but not yet consumed
       */
      public DMLSelectStatement select() throws SQLSyntaxErrorException {
          return new MySQLDMLSelectParser(lexer, exprParser).select();
      }
  
  }
  