Пришло время затронуть тему создания парсеров на 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="">)+("#")+>} </comment:> |
Далее в методе init() мы перебираем команды для создания таблиц, их может быть 0 и более — это задается конструкцией (…)*. В коде есть несколько закоментированных команд вывода в стандартный поток, они помогают понять что и когда вызывается.
В методе Variables() мы обрабатываем перечисление описаний столбцов при создании таблицы. Тут появляется на сцене рекурсия: перечесление может быть описанием одной переменной без запятой на конце, либо перечислением + запятая + описание. Вообще обработка отличается от обработки в yacc.
В общем случае в фигурных скобках задаются действия, которые надо выполнить при встрече в потоке определенных токенов или их последовательности:
( TName = <tname> typeDesc = DType() {var.put(TName.image, typeDesc); /*System.out.println("new struct populated < " + TName.image + ">");*/} ( <comma> TName = <tname> typeDesc = DType() {var.put(TName.image, typeDesc); /*System.out.println("[Recursion] new struct populated < " + TName.image + ">");*/} )* ) </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="">)+("#")+>} 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 < " + TName.image + ">");*/} ( <comma> TName = <tname> typeDesc = DType() {var.put(TName.image, typeDesc); /*System.out.println("[Recursion] new struct populated < " + TName.image + ">");*/} )* ) {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 < " + length.image + ">");*/ } 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 < 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<Ыtring ,TypeDesc> 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> |