Это продолжение Части 1.
После того как мы разобрались с каждым уровнем BeanShell-подстановок по отдельности, посмотрим, как эти уровни согласуются друг с другом при совместном использовании. Здесь пойдет речь пока только о тесном сотрудничестве разных и одинаковых подстановок, когда они буквально проникают друг в друга. Как происходит их интерпретация, когда они вложены друг в друга?
Невозможность пересечения подстановок
Это самое очевидное правило. Оно здесь упоминается только для полноты картины. Вот некорректный пример:
<? /* подстановка 1 начало */
<% /* подстановка 2 начало */
/* подстановка 1 конец */ ?>
/* подстановка 2 конец */ %>
Такой код приведёт к ошибке. Подстановки могут быть вложены только иерархически, аналогично элементам в XML-документе.
<? /* подстановка 1 начало */
<% /* подстановка 2 начало */
/* подстановка 2 конец */ %>
/* подстановка 1 конец */ ?>
Более ранние внутри более поздних
Самый простой и естественный способ вложения подстановок — поместить более ранние внутрь более поздних. При выполнении ранние превратятся в текст или будут изъяты (если код подстановки ничего не выводит в выходной поток, то результатом выполнения будет изъятие подстановки). Более поздняя подстановка остаётся «чистой», без артефактов, нарушающих синтаксис. Например, код
<?
String v_sess_info = "Сессия <%=odiRef.getSession("SESS_NO")%>; шаг <%=odiRef.getSession("NNO")%>";
?>
после первого прохода даст примерно следующее:
<?
String v_sess_info = "Сессия 123123123555; шаг 3";
?>
Тут без выполнения конкатенации строк результаты odiRef-методов внедряются в текст, который позже присваивается переменной. В документации можно видеть, что функции SubstitutionAPI — это методы объекта odiRef, возвращающие чаще всего String. Однако нигде вы не найдёте прямого указания, что та или иная функция может выполняться только на определённых уровнях подстановки. Более того, одна и та же функция с одними аргументами может работать на одном уровне, а с другими — только на другом. Об этом мы поговорим в одной из следующих статей.
Так что в ситуации, когда, например, на уровне «@» требуется получить значение, которое SubstitutionAPI способно вернуть только с помощью %-подстановки, то вложение подстановок является единственным способом получения желаемого результата.
Более поздние внутри более ранних
Если интерпретатор, выполняющий, например, ?-подстановку внезапно натолкнётся на «<@», то он прекратит выполнение по ошибке. Естественно, ведь он ожидает увидеть тут BeanShell-код. То есть обратное вложение сделать вроде бы нельзя… Но всё же можно.
Если мы спрячем код подстановки вместе с метасимволами внутрь текстовой строки и выведем эту строку в поток, то отработавшая ?-подстановка оставит после себя в коде более позднюю @-подстановку. При следующих проходах она успешно выполнится. Например:
<?/* функция нестандартной обработки имён полей */
String fieldProcessing(String fldName, String srcTyp, String dstTyp){
...
return changedFldName;
}?>
...
<%=odiRef.getColList(0,"t","<?=fieldProcessing("+'"'+"[COL_NAME]"+'"'+","+'"'+"[SOURCE_DT]"+'"'+","+'"'+"[DEST_DT]"+'"'+")?>","nt,","")%>
...
У функции odiRef.getColList и ей подобных есть множество различных мнемоник, позволяющих обратиться к именам, типам, формату полей, комментариям полей введённых в модели данных, и другим атрибутам. Но вся логика генерации кода на основании списка полей находится внутри SubstitutionAPI, и, казалось бы, уже нельзя вмешаться в неё. А в редких случаях может понадобиться:
- удалить или добавить префиксы или инфиксы в именах полей;
- изменить результат интерпретации в зависимости от типа или соотношения типов источника и цели;
- использовать заголовок поля, если он введён в модели. Но если не введён, то использовать имя поля (это может быть полезно, например, для генерации заголовка CSV-файла);
- обеспечить особые правила обработки для полей, отмеченных в модели каким-либо признаком;
- и другие виртуозные задачи.
Для этого нужно, чтобы при генерации кода получился промежуточный результат, который при следующем выполнении подстановок даст требуемый результат. Приведённый выше пример после первого прохода даст что-нибудь вроде
<?/* функция нестандартной обработки имён полей */
String fieldProcessing(String fldName, String srcTyp, String dstTyp){
...
return changedFldName;
}?>
...
<?=fieldProcessing("FIELD1","NUMBER","NUMBER")?>,
<?=fieldProcessing("FIELD2","NUMBER","CHAR")?>,
<?=fieldProcessing("FIELD3","CHAR","CHAR")?>,
...
Если встраиваемая логика проста, то её можно реализовать прямо внутри строки при вызове odiRef.getColList. Но представление кода в строке сопряжено с рядом проблем (даже двойные кавычки внутри строки могут привести к неприятностям, потому что из-за многопроходной интерпретации u0022 работает далеко не всегда). Поэтому лучше эту логику вынести в функцию, которую и вызывать.
Топтание на месте
Если одна подстановка может «напечатать» другую подстановку, и последняя будет работать, то нельзя ли это сделать, оставаясь на том же уровне подстановок? Оказывается, можно. Есть основания считать, что некоторые функции SubstitutionAPI сами пользуются этой возможностью. Например, функция odiRef.getOption, используемая на уровне «%», может давать результат (значение опции), в котом также содержатся %-подстановки. И они прекрасно работают! Это самый очевидный пример. Но провоцируя различного рода ошибки можно заметить по логам, как на уровнях «%» и «?» odiRef-функции незримо превращаются в snpRef, и иногда это происходит при переходе «%» на «?», но сколько выполняется таких итераций, мы не знаем.
Попробуем зациклить интерпретатор:
<%
Long n = 0L;
String code1="/*<"+"%=n%"+">*/";
String code2="<"+"%"+"if(n++>10){out.print(code1);}else{out.print(code2);}"+"%"+">";
%>
<%=code2%>
Пока n не больше 10, происходит вывод одного и того же кода. Но он тоже является %-подстановкой, и всё начинается сначала. Итеративная интерпретация кода завершается при n=11. Тогда будет напечатан комментарий со значением n. Финальный код, который мы увидим в Операторе ODI: «/*12*/». Если в этом примере вместо 10 поставить 100, то результат циклической интерпретации будет таким:
### KEY com.sunopsis.res.gen / ODI-15015: Too much recursion in this text resolution.### (Command 0)
out.print("n") ;
if(n++>100){out.print(code1);}else{out.print(code2);}
****** ORIGINAL TEXT ******
<%if(n++>100){out.print(code1);}else{out.print(code2);}%>
То есть предельное количество итераций находится между 10 и 100. Можно вписать этот код в процедуру, включив на этом шаге опцию Ignore Errors, а на следующем шаге вывести значение переменной n (например, так: /* <%=n%> */). Тогда мы узнаем, какова же реальная глубина таких рекурсивных подстановок. Она, на удивление, не велика.
Кстати, обратите внимание, что исключение, возникшее при выполнении %-подстановки, не прекратило работу сессии, потому что по логам косвенно понятно, что падение произошло позже, при финальном выполнении, что следует из ошибки «Token Parsing Error: Lexical error at line 1, column 2. Encountered: "#"».
Последний пример не имеет практической ценности, но даёт ключ к понимаю, как устроен механизм подстановок.
В заключение остаётся лишь сказать, что нельзя из более поздних подстановок напечатать код, содержащий более раннюю подстановку. То есть сделать это можно, но вставленная таким образом подстановка останется как есть до финального выполнения и «сломает» синтаксис уже там, потому что соответствующий интерпретатор тут уже побывал, и чинно стуча копытами перешёл к следующему шагу сессии. Он, как пешка, ходит только вперёд, и бьёт наискосок. И мы тоже удаляемся для написания следующей части, где будет рассмотрена условная интерпретация и даны примеры её использования, в том числе для выгрузки данных в сложноструктурированные файлы.
Автор: Райффайзенбанк