В июне этого года, уже местное руководство, осчастливило меня новым активационным проектом, в котором маршрутизаторы Cisco сменились на АТС Alcatel S12 и M200. Вторым отличием этого проекта было то, что всю активационную часть предстояло разработать мне одному, с нуля, в течение полугода.
Разумеется, у меня и мысли не возникло использовать для активационных скриптов JavaScript или что-то подобное. Мне был необходим бюджетный вариант скриптинга. После некоторых размышлений, я решил хранить скрипты активации в БД Oracle, используя для хранения следующую структуру:
Из этой схемы видно, что части скрипта (AE_SCRIPT_PART), представляющие собой скрипт (AE_SCRIPT) или команду (AE_COMMAND), связываются в иерархическую структуру, посредством таблицы AE_SUBSCRIPT (первоначально AE_SCRIPT_PART просто содержала поле PARENT, ссылающееся на эту-же таблицу, но такое решение препятствовало повторному использованию фрагментов скриптов). Таблица AE_SETTING служит для привязки к скриптам и командам значений ряда настроек, определенных AE_SETTING_TYPE, таких как условие выполнения (if_condition) или переменная цикла (for_each).
Первоначально была предусмотрена возможность создания шаблонов команд (AE_COMMAND_TEMPLATE), связанных с набором заранее определенных значений настроек, но эта возможность оказалась избыточной и, в настоящее время, почти не используется. Фактически, эта схема данных позволяет хранить AST.
Чтобы не заблудиться во всем этом безобразии, параллельно наполнению скрипта в БД, велся Excel-файл следующего вида:
Это давало возможность вести разработку, но передать эту работу (или часть работ) другому сотруднику, в таком виде, разумеется, не представлялось возможным. Конечно, я начал задумываться о более дружественном скриптинге.
Около месяца назад, меня попросили провести ряд занятий, посвященных архитектуре нового активационного проекта (не только скриптинга, а всего проекта в целом). При разработке одной из презентаций, была нарисована следующая картинка:
Текст в нижней стрелочке и послужил прототипом разрабатываемого скриптового языка. Я просто подумал, что было бы неплохо, используя Perl, например, вынуть описание скрипта из БД и представить в компактном, читабельном виде, для того чтобы проверить его на отсутствие ошибок.
Потом я подумал еще немного и решил использовать PL/SQL, а не Perl. Действительно, одним вызовом хранимой процедуры, я мог сформировать текст скрипта в CLOB, и это было ничуть не хуже чем скрипт в текстовом файле. Вот что получилось в результате:
create or replace package body ae_scripting as g_if_setting constant varchar2(100) default 'if_condition'; g_foreach_setting constant varchar2(100) default 'foreach_var'; g_target_setting constant varchar2(100) default 'target_platform'; g_if_statement constant varchar2(100) default 'if'; g_foreach_statement constant varchar2(100) default 'foreach'; procedure extract(p_id in number) as cursor c_cmd is select d.id, d.nm, d.lv, d.type_id, p.name platform, t0.value target, f.direction_id direction from ( select level lv, sys_connect_by_path(to_char(c.nm, '00000'), '/') pt, sys_connect_by_path(c.id, '.') nm, c.id id, c.type_id from ( select a.id id, a.type_id type_id, b.parent_id parent_id, b.order_num nm from ae_script_part a, ae_subscript b where b.child_id(+) = a.id ) c start with c.id = p_id connect by prior c.id = c.parent_id order by pt ) d left join ae_script e on (e.id = d.id) left join ae_command f on (f.id = d.id) left join ae_platform p on (p.id = e.platform_id) left join ae_setting t0 on (t0.object_id = d.id and t0.setting_type_id = 1000001); r_cmd c_cmd%rowtype; cursor c_par(p_cmd_id number) is select t.name name, s.value value from ae_script_part a inner join ae_setting s on (s.object_id = a.id) inner join ae_setting_type t on (t.id = s.setting_type_id) where a.id = p_cmd_id union all select t.name, s.value from ae_script_part a inner join ae_command c on (c.id = a.id) inner join ae_setting s on (s.object_id = c.template_id) inner join ae_setting_type t on (t.id = s.setting_type_id) where a.id = p_cmd_id; r_par c_par%rowtype; l_str varchar2(1000) default null; l_lob CLOB; l_lvl number default 0; l_plv number default 0; l_cnt number default null; l_stt varchar2(50) default null; begin delete from ae_script_src where id = p_id; insert into ae_script_src(id, text) values (p_id, empty_clob()); select text into l_lob from ae_script_src where id = p_id; dbms_lob.open(l_lob, dbms_lob.lob_readwrite); open c_cmd; loop fetch c_cmd into r_cmd; exit when c_cmd%notfound; while r_cmd.lv <= l_plv loop l_str := ' ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10); dbms_lob.writeappend(l_lob, length(l_str), l_str); l_plv := l_plv - 1; l_lvl := l_lvl - 1; end loop; l_str := '[' || trim(to_char(r_cmd.id, '000000')) || '] '; if not r_cmd.direction is null then if r_cmd.direction = '1' then l_str := l_str || '<'; else l_str := l_str || '>'; end if; else l_str := l_str || ' '; end if; l_str := l_str || lpad(' ', 2 * r_cmd.lv); if not r_cmd.platform is null then l_str := l_str || 'platform:' || r_cmd.platform || '; '; end if; if not r_cmd.target is null then l_str := l_str || 'target:' || r_cmd.target || '; '; end if; open c_par(r_cmd.id); l_stt := null; loop fetch c_par into r_par; exit when c_par%notfound; if l_stt is null and r_par.name = g_if_setting then l_str := l_str || g_if_statement || ' (' || r_par.value || ') { '; l_stt := r_par.name; end if; if l_stt is null and r_par.name = g_foreach_setting then l_str := l_str || g_foreach_statement || ' (' || r_par.value || ') { '; l_stt := r_par.name; end if; end loop; close c_par; open c_par(r_cmd.id); loop fetch c_par into r_par; exit when c_par%notfound; if l_stt is null or l_stt <> r_par.name then if r_par.name <> g_target_setting then l_str := l_str || r_par.name || ':' || r_par.value || '; '; end if; end if; end loop; close c_par; select count(*) into l_cnt from ae_subscript where parent_id = r_cmd.id; if r_cmd.type_id = 1 and l_cnt > 0 then if l_stt is null then l_str := l_str || '{'; end if; l_lvl := l_lvl + 1; l_plv := r_cmd.lv; else if not l_stt is null then l_str := l_str || ' }'; end if; l_plv := r_cmd.lv - 1; end if; l_str := l_str || chr(13) || chr(10); dbms_lob.writeappend(l_lob, length(l_str), l_str); end loop; close c_cmd; while l_lvl > 0 loop l_str := ' ' || lpad('}', 2 * l_lvl) || chr(13) || chr(10); dbms_lob.writeappend(l_lob, length(l_str), l_str); l_lvl := l_lvl - 1; end loop; dbms_lob.close(l_lob); commit; exception when others then if c_cmd%isopen then close c_cmd; end if; if c_par%isopen then close c_par; end if; if dbms_lob.isopen(l_lob) = 1 then dbms_lob.close(l_lob); end if; rollback; raise; end; end ae_scripting; /
После построения дерева скрипта connect by запросом, остальное почти тривиально. Некоторые сложности были связаны только с генерацией правильной последовательности процедурных скобок ‘{‘ и ‘}’ определяющих вложенность скриптов. Для настроек if_condition и foreach_var генерируются более привычные формы операторов if и foreach.
Вот что получается в результате работы этой хранимки (весь скрипт целиком я не привожу):
[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; } }
Каждая строка скрипта (за исключением закрывающихся процедурных скобок) определяет скрипт или команду. Команда определяется символами ‘<‘ и ‘>’, определяющими направление передачи данных (на оборудование и с него). Настройки определяются следующей последовательностью:
<Имя настройки>:<Значение>;
Символ ‘;’ является разделителем настроек и не должен использоваться внутри значения настройки (вообще говоря, в командах S12 этот символ используется в качестве разделителя команд, но я завершаю команды символом ‘.’ и необходимости использования ‘;’ внутри команды не возникает). В любом случае, добавить экранирование служебных символов внутри значений совсем не сложно.
Важной, но не обязательной частью скрипта являются числа в квадратных скобках. Это рекомендуемые значения ID для размещения скрипта или команды в БД. Задав одинаковое значение ID для команд или скриптов, можно добиться повторного использования фрагмента скрипта (при условии того, что помеченные фрагменты действительно идентичны), разместив этот фрагмент в БД однократно. Если значение ID не задано, оно назначается автоматически, при загрузке скрипта в БД.
Итак, я научился извлекать скрипты из описания в БД и представлять их в компактной и наглядной форме, но апетит приходит во время еды, и хочется уже большего. Действительно, как здорово было-бы иметь возможность исправить скрипт в текстовой форме и загрузить обратно в БД.
В следующей статье, я займусь реализацией этой возможности.
ссылка на оригинал статьи http://habrahabr.ru/post/162865/
Добавить комментарий