Скриптинг для бюджетной активации (часть 2)

в 14:02, , рубрики: DBMS_LOB, oracle, PL/SQL, scripting, ненормальное программирование, Скриптинг, метки: , , ,

Ранее я уже писал о том как решал проблему представления скриптов в удобочитаемом виде в одном активационном проекте, в сфере традиционной телефонии. Напомню, что речь шла о передаче управляющих команд на АТС Alcatel S12 и M200 через Serial или TCP-соединение. Несмотря на всю «велосипедность» описанного подхода, он полностью себя оправдал. Уже первый просмотр сгенерированного скрипта, позволил обнаружить и исправить серьезные ошибки в логике активации, поиск которых непосредственно в таблицах AST-представления занял бы гораздо больше времени.

В настоящее время, проект перешел в фазу отладки активационной логики, предусматривающую гораздо более интенсивное изменение активационных скриптов. В таких условиях, наличие инструмента, позволяющего сформировать текстовое представление активационного скрипта, а также залить скрипт обратно в БД, после внесения изменений, позволит значительно увеличить продуктивность работы. Проблеме синтаксического разбора скрипта и загрузки его в AST-представление в БД и посвящена эта статья.

Напомню, что скрипт выглядит следующим образом:

1420.ae
[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, аналогичного средства не предусмотрено. Впрочем, задача эта более громоздкая, чем сложная. Наш сканер будет выглядеть следующим образом:

ae_scripting.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 по мере обнаружения лексем.

На этом этапе, полезно убедиться, что сканер действительно находит в скрипте требуемые лексемы, в нужной нам последовательности. Сделать это просто. Создадим таблицу-лог:

ae_tables.sql

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 следующим образом:

ae_scripting.sql

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js