Использование javacc для разбора SQL

Пришло время затронуть тему создания парсеров на java. Для этого воспользуемся утилитой javacc.

Update. Плагин Вордпресса для подсвечивания кода стал безнадежно портить исходники — добавлять закрывающие теги там, где они не нужны, дефолтные значения у того, что он считает атрибутами тегов. Неискаженные исходники, адаптированные к javaCC версии 7.0.9 доступны тут: исходники

За основу взята эта статья с примерами: источник моего вдохновения. В коде примеров исправлены ошибки и теперь они даже собираются 😉
Код доработан и теперь, к примеру, правильно обрабатывает запятую в перечислении: она допустима тогда и только тогда, когда за текущим элементом перечисления есть еще один или несколько элементов. Если для типа данных в скобках указана длина (опциональный параметр), то она тоже будет сохранена в результирующей структуре для описания типа.

Для простоты я закомментировал название пакета — если надо, раскоментируйте и ставьте свой.
Я собирал с помощью make:

JAVACC_JAR = /home/dk/javacc_6_1/javacc-6.1.0/target/javacc-6.1.0.jar
SOURCE_NAME=grammar
 
all: TableStruct.java TypeDesc.java ParserDemoTest.java SqlParser.java
	javac *.java	 
 
SqlParser.java : $(SOURCE_NAME).jj
	java -cp $(JAVACC_JAR) javacc $(SOURCE_NAME).jj
 
clean: 
	rm -rf ./ParseException.*
	rm -rf ./SimpleCharStream.*
	rm -rf ./SqlParser.*
	rm -rf ./SqlParserConstants.*
	rm -rf ./SqlParserTokenManager.*
	rm -rf ./Token.*
	rm -rf ./TokenMgrError.*
	rm -rf ./*.class

Все исходники лежат в текущей папке и название пакета не нужно.

Разбирать будем файлы вида:

##JavaccParserExample#####
CREATE TABLE STUDENT 
( 
    StudentName varchar (20), 
    Class varchar(10),
    Rnum integer
) 
 
CREATE TABLE BOOKS
( 
    BookName varchar(10), 
    Edition integer, 
    Stock integer
) 
CREATE TABLE CODES
( 
    StudentKey varchar(20), 
    StudentCode varchar(40)
)

В первой строке мы видим комментарий, который будет игнорироваться благодаря этой инструкции:

SPECIAL_TOKEN : {<comment: ("#")+(<tname="">)+("#")+&gt;}
</comment:>

Далее в методе init() мы перебираем команды для создания таблиц, их может быть 0 и более — это задается конструкцией (…)*. В коде есть несколько закоментированных команд вывода в стандартный поток, они помогают понять что и когда вызывается.

В методе Variables() мы обрабатываем перечисление описаний столбцов при создании таблицы. Тут появляется на сцене рекурсия: перечесление может быть описанием одной переменной без запятой на конце, либо перечислением + запятая + описание. Вообще обработка отличается от обработки в yacc.
В общем случае в фигурных скобках задаются действия, которые надо выполнить при встрече в потоке определенных токенов или их последовательности:

  ( 
      TName = <tname>
      typeDesc = DType()
      {var.put(TName.image, typeDesc); /*System.out.println("new struct populated &lt; " + TName.image + "&gt;");*/}
      (   <comma> TName = <tname> 
          typeDesc = DType() 
          {var.put(TName.image, typeDesc); /*System.out.println("[Recursion] new struct populated &lt; " + TName.image + "&gt;");*/}
      )*
  )
</tname></comma></tname>

Т.е. мы можем встретить имя с описанием всего один раз и тогда сразу заносим это в коллекцию. В случае, если у нас идет перечисление с запятыми, то мы каждую также заносим в коллекцию.

Длина типа данных является необязательной и это отражено в соответствующем блоке кода (квадратные скобки для необязательной конструкции):

  (
    TDType=<tname>
    [<obra>length=<number><cbra>]
  )
</cbra></number></obra></tname>

Признаком того, что тип указан не был, является null в переменной length.
Для получения строкового представления токена нужно запросить его свойство image:

TDType.image

Полное содержание файла grammar.jj:

/* demo grammar.jj*/
options
{
    STATIC = false;
}
PARSER_BEGIN (SqlParser)
   //package ru.outofrange.javacc.createtable;
   import java.util.ArrayList;
   import java.util.HashMap;
 
   class SqlParser {
         ArrayList<tablestruct> initParser() throws ParseException, TokenMgrError { 
             return(init()) ; 
         }
   }
PARSER_END (SqlParser)
 
SKIP: { "\n" | "\r" | "\r\n" | "\\" | "\t" | " "}
 
TOKEN [IGNORE_CASE]:
{
 <ctcmd :("create="" table")="">
|<number :(["0"-"9"])+="">
|<tname: (["a"-"z"])+="">
|<obra: ("(")+="">
|<cbra: (")")+="">
|<comma: (",")="">
}
 
SPECIAL_TOKEN : {<comment: ("#")+(<tname="">)+("#")+&gt;}
 
ArrayList<tablestruct> init():
{
  Token T;
  ArrayList<tablestruct> tableList = new ArrayList<tablestruct>();
  TableStruct tableStruct;
}
{
  (
       <ctcmd>
       T =<tname>
       {    tableStruct = new TableStruct ();
       tableStruct.TableName = T.image ;}
       <obra>
       tableStruct.Variables = Variables()
       <cbra>
      {tableList.add (tableStruct) ; /*System.out.println("new variable desc added");*/}
  )*
  <eof>
  {return tableList;}
}
 
HashMap<string ,typedesc=""> Variables():
{
   Token TName;
   TypeDesc typeDesc;
   HashMap <string, typedesc=""> var = new HashMap <string, typedesc="">();
}
{
  ( 
      TName = <tname>
      typeDesc = DType()
      {var.put(TName.image, typeDesc); /*System.out.println("new struct populated &lt; " + TName.image + "&gt;");*/}
      (   <comma> TName = <tname> 
          typeDesc = DType() 
          {var.put(TName.image, typeDesc); /*System.out.println("[Recursion] new struct populated &lt; " + TName.image + "&gt;");*/}
      )*
  )
  {return var;}
}
 
TypeDesc DType():
{
   Token TDType;
   TypeDesc typeDesc;
   Token length = null;
}
{
  (
    TDType=<tname>
    [<obra>length=<number><cbra>]
  )
   {
       typeDesc = new TypeDesc();
       typeDesc.setTypeName(TDType.image);
       if (length != null) {
           typeDesc.setTypeLength(length.image); /*System.out.println("Lenght detected &lt; " + length.image + "&gt;");*/
       }
 
       return typeDesc;
   }
}
</cbra></number></obra></tname></tname></comma></tname></string,></string,></string></eof></cbra></obra></tname></ctcmd></tablestruct></tablestruct></tablestruct></comment:></comma:></cbra:></obra:></tname:></number></ctcmd></tablestruct>

Вспомогательные классы:

//package ru.outofrange.javacc.createtable;
 
public class TypeDesc {
 
        private String typeNameName; 
        private Integer typeLength = null;
 
        public void setTypeLength(Integer typeLength){
            this.typeLength = typeLength;
        }
 
        public void setTypeLength(String typeLength){
            this.typeLength = Integer.valueOf(typeLength);
        }
 
        public void setTypeName(String typeNameName){
            this.typeNameName = typeNameName;
        }
 
        public String getTypeName(){
            return typeNameName;
        }
 
        public Integer getTypeLength(){
            return typeLength;
        }
}

 

//package ru.outofrange.javacc.createtable;
 
import java.util.HashMap;
import java.util.Map;
 
public class TableStruct { 
  String TableName; 
  Map<string, typedesc=""> Variables = 
              new HashMap<string, typedesc=""> ();
}
</string,></string,>

Класс с функцией main() для тестирования парсера:

 /*for testing the parser class*/ 
 
//package ru.outofrange.javacc.createtable;
 
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Map;
 
public class ParserDemoTest { 
public static void main(String[] args) { 
    try{ 
        if (args.length &lt; 1) {
            return;
        }
        String filePath = args[0];
 
        SqlParser parser = new SqlParser (new FileReader(filePath));  
        ArrayList<tablestruct> tableList = parser.initParser();
 
        for(TableStruct t1 : tableList) {  
            System.out.println("--------------------------"); 
            System.out.println("Table Name : " + t1.TableName); 
            for (Map.Entry&lt;Ыtring ,TypeDesc&gt; entry: t1.Variables.entrySet()){
                System.out.print("Field name: " + entry.getKey() + " Data Type: " + entry.getValue().getTypeName());
                if (entry.getValue().getTypeLength() != null) {
                    System.out.println(" Length: " + entry.getValue().getTypeLength());
                } else {
                    System.out.println(" Length: <not specified="">");
                }
            }
            System.out.println("--------------------------");
        } 
    } catch (Exception ex) {
        ex.printStackTrace() ;} 
    } 
}
</not></tablestruct>

Результат выполнения программы:

java ParserDemoTest ./sql_test
 
--------------------------
Table Name : STUDENT
Field name: Rnum Data Type: integer Length: <not specified="">
Field name: Class Data Type: varchar Length: 10
Field name: StudentName Data Type: varchar Length: 20
--------------------------
--------------------------
Table Name : BOOKS
Field name: BookName Data Type: varchar Length: 10
Field name: Edition Data Type: integer Length: <not specified="">
Field name: Stock Data Type: integer Length: <not specified="">
--------------------------
--------------------------
Table Name : CODES
Field name: StudentCode Data Type: varchar Length: 40
Field name: StudentKey Data Type: varchar Length: 20
--------------------------
</not></not></not>
You can leave a response, or trackback from your own site.

Leave a Reply