Ранее я уже писал о том как решал проблему представления скриптов в удобочитаемом виде в одном активационном проекте, в сфере традиционной телефонии. Напомню, что речь шла о передаче управляющих команд на АТС Alcatel S12 и M200 через Serial или TCP-соединение. Несмотря на всю «велосипедность» описанного подхода, он полностью себя оправдал. Уже первый просмотр сгенерированного скрипта, позволил обнаружить и исправить серьезные ошибки в логике активации, поиск которых непосредственно в таблицах AST-представления занял бы гораздо больше времени.
В настоящее время, проект перешел в фазу отладки активационной логики, предусматривающую гораздо более интенсивное изменение активационных скриптов. В таких условиях, наличие инструмента, позволяющего сформировать текстовое представление активационного скрипта, а также залить скрипт обратно в БД, после внесения изменений, позволит значительно увеличить продуктивность работы. Проблеме синтаксического разбора скрипта и загрузки его в AST-представление в БД и посвящена эта статья.
Напомню, что скрипт выглядит следующим образом:
[001420] target:ats.type; foreach (params) {
[003101] platform:S-12; if (dou_off.dou = 'REDIRECT_NOANSWER') {
[031010] < text:MODIFY-SUBSCR:DN=K'%s,CFWD=DEACT&CFWDNOR.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003111] platform:S-12; if (dou_off.dou = 'REDIRECT_BUSY') {
[031110] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDBSUB.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003121] platform:S-12; if (dou_off.dou = 'REDIRECT_AUTOINF') {
[031210] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDFIXA.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003131] platform:S-12; if (dou_off.dou = 'REDIRECT') {
[031310] < text:MODIFY-SUBSCR:DN=K'%s, CFWD=DEACT&CFWDUVAR.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001008] platform:M-200; var_list:is_redirect_param = 1;
}
[003071] platform:S-12; if (dou_off.dou = 'SET_ALARM_CLOCK') {
[030710] < text:MODIFY-SUBSCR:DN=K'%s,ALMCALL=DEACT.; var_list:phone;
[001041] > regexp:(.+); var_list:error_text; is_error:1;
[001009] var_list:is_alarm_param = 1;
}
}
Каждая строка скрипта (за исключением закрывающихся процедурных скобок) определяет скрипт или команду. Команда определяется символами '<' и '>', определяющими направление передачи данных (на оборудование и с него). Со скриптом или командой могут быть связаны настройки, определяемые следующей последовательностью:
<Имя настройки>:<Значение>;
Для настроек 'if_condition' и 'foreach_var', предусмотрены специальные синтаксические конструкции 'if' и 'foreach' похожие на аналогичные операторы привычных нам императивных языков.
Важной, но не обязательной частью скрипта являются числа в квадратных скобках. Это рекомендуемые значения ID для размещения скрипта или команды в БД. Задав одинаковое значение ID для команд или скриптов, можно добиться повторного использования фрагмента скрипта (при условии того, что помеченные фрагменты действительно идентичны), разместив этот фрагмент в БД однократно. Если значение ID не задано, оно назначается автоматически, при загрузке скрипта в БД.
Первым шагом в разборе скрипта будет его лексический анализ. Нам необходимо разработать процедуру-сканер, последовательно просматривающую поток символов, читаемых DBMS_LOB и формирующую на выходе последовательность лексем.
Если бы мы разрабатывали парсер на языке C, мы могли бы использовать Lex для генерации сканера. К сожалению, для PL/SQL, аналогичного средства не предусмотрено. Впрочем, задача эта более громоздкая, чем сложная. Наш сканер будет выглядеть следующим образом:
create or replace package body ae_scripting as
g_init_state constant number default 0;
g_id_state constant number default 1;
g_ch_state constant number default 2;
g_name_state constant number default 3;
g_value_state constant number default 4;
e_syntax_error EXCEPTION;
pragma EXCEPTION_INIT(e_syntax_error, -20001);
...
procedure load(p_id in number) as
l_lob CLOB;
l_str varchar2(1000) default null;
l_len number default null;
l_pos number default 1;
l_ix number default 1;
l_state number default g_init_state;
l_ch varchar2(1) default null;
l_lexem varchar2(1000) default null;
l_lvl number default 0;
l_isb number default 0;
l_cmd number default 0;
l_prev varchar2(1000) default null;
begin
select text into l_lob from ae_script_src where id = p_id for update;
dbms_lob.open(l_lob, dbms_lob.lob_readonly);
l_len := dbms_lob.getlength(l_lob);
while l_pos <= l_len loop
l_str := dbms_lob.substr(l_lob, 1000, l_pos);
l_ix := 1;
while l_ix <= length(l_str) loop
l_ch := substr(l_str, l_ix, 1);
if l_lvl > 0 then
if l_lvl = 1 and l_ch = ')' then
l_prev := '';
lexem(g_value_state, l_lexem);
l_lexem := '';
l_lvl := 0;
else
if l_ch = '(' then
l_lvl := l_lvl + 1;
end if;
if l_ch = ')' then
l_lvl := l_lvl - 1;
end if;
l_lexem := l_lexem || l_ch;
end if;
elsif l_lvl = 0 and l_ch = '(' and l_state = g_init_state then
l_prev := '';
l_lvl := 1;
l_lexem := '';
elsif l_ch = '{' then
if l_state <> g_init_state then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
l_isb := 1;
elsif l_ch = '}' and l_isb = 1 then
if l_state <> g_init_state then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
l_isb := 0;
elsif l_ch = '<' or l_ch = '>' or l_ch = '}' or l_ch = chr(13) then
if l_ch = '<' or l_ch = '>' then
l_cmd := 1;
end if;
if l_state <> g_init_state then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
if l_ch = chr(13) and l_isb = 1 then
lexem(g_ch_state, '{');
l_prev := '';
l_isb := 0;
l_cmd := 0;
end if;
if l_ch <> '}' or l_cmd = 0 then
if l_prev is null or l_prev <> l_ch then
lexem(g_ch_state, l_ch);
end if;
if l_ch = chr(13) then
l_prev := l_ch;
else
l_prev := '';
end if;
end if;
if l_ch = '}' then
lexem(g_ch_state, l_ch);
end if;
l_lexem := '';
l_state := g_init_state;
elsif l_ch = '[' then
if l_state <> g_init_state then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
l_lexem := '';
l_state := g_id_state;
elsif l_ch = ']' then
if l_state <> g_id_state or l_lexem is null then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
lexem(l_state, l_lexem);
l_prev := '';
l_lexem := '';
l_state := g_init_state;
elsif l_ch = ':' then
if l_state = g_value_state then
l_lexem := l_lexem || l_ch;
else
if l_state <> g_init_state then
lexem(l_state, l_lexem);
l_prev := '';
end if;
l_lexem := '';
l_state := g_value_state;
end if;
elsif l_ch = ';' then
if l_state <> g_value_state then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
lexem(l_state, l_lexem);
l_prev := '';
l_lexem := '';
l_state := g_init_state;
elsif l_ch = ' ' or l_ch = chr(9) or l_ch = chr(10) then
if l_state = g_value_state then
l_lexem := l_lexem || ' ';
else
if l_state <> g_init_state then
lexem(l_state, l_lexem);
l_prev := '';
end if;
l_lexem := '';
l_state := g_init_state;
end if;
else
if l_state = g_id_state or
l_state = g_name_state or
l_state = g_value_state then
l_lexem := l_lexem || l_ch;
elsif l_state = g_init_state then
l_lexem := l_ch;
l_state := g_name_state;
end if;
end if;
l_ix := l_ix + 1;
end loop;
l_pos := l_pos + 1000;
end loop;
dbms_lob.close(l_lob);
if g_level <> 0 then
RAISE_APPLICATION_ERROR(-20001, 'Syntax error');
end if;
exception
when others then
if dbms_lob.isopen(l_lob) = 1 then dbms_lob.close(l_lob); end if;
raise;
end;
...
end ae_scripting;
/
Несмотря на устрашающие размеры, этот код довольно прост. Мы последовательно просматриваем все символы CLOB-поля, изменяя состояние нескольких переменных и вызывая процедуру lexem по мере обнаружения лексем.
На этом этапе, полезно убедиться, что сканер действительно находит в скрипте требуемые лексемы, в нужной нам последовательности. Сделать это просто. Создадим таблицу-лог:
create sequence ae_script_lex_log_seq;
create table ae_script_lex_log (
id number not null,
state_id number,
text varchar2(2000)
);
create unique index ae_script_lex_log_pk on ae_script_lex_log(id);
alter table ae_script_lex_log add
constraint pk_ae_script_lex_log primary key(id);
и определим процедуру lexem следующим образом:
create or replace package body ae_scripting as
...
procedure lexem(p_state in number, p_value in varchar2) as
begin
insert into ae_script_lex_log(id, type_id, text)
values (ae_script_lex_log_seq.nextval, p_state, p_value);
end;
...
end ae_scripting;
/
Теперь мы можем разобрать скрипт в CLOB-поле процедурой load и визуально убедиться в том, что лексемы найдены в требуемом порядке и содержат ожидаемые нами значения.
В следующей статье, мы рассмотрим как можно использовать полученный поток лексем для загрузки скрипта в базу данных.
Автор: GlukKazan