Обращение к внешней системе (EXTERNAL)
Платформа предоставляет возможность обращаться к внешним системам с использованием различных типов взаимодействия / протоколов, как внешний аналог внутреннего вызова. Интерфейсом такого обращения является выполнение кода на языке / в парадигме внешней системы с заданными параметрами и, при необходимости, возврат некоторых значений в качестве результатов, записываемых в заданные свойства (без параметров). Предполагается, что все объекты параметров и результатов являются объектами встроенных классов.
Типы взаимодействий / протоколы
На данный момент в платформе поддерживаются следующие типы взаимодействий / внешних систем:
HTTP - выполнение http-запроса Web-сервера
Для этого типа взаимодействия задается только строка запроса (URL), которая одновременно определяет как адрес сервера, так и непосредственно запрос, который необходимо выполнить. HTTP-метод (GET, POST, PUT, DELETE, PATCH) задаётся отдельно; по умолчанию используется POST. По умолчанию запрос выполняется на сервере приложений, но может быть перенаправлен на клиент пользователя — это полезно, когда адрес доступен с клиента, но не с сервера.
Таймаут вызова и строгость проверки SSL читаются из свойств System.timeoutHttp[] (в миллисекундах, со встроенным значением по умолчанию) и System.insecureSSL[] (при истинном значении отключает проверку TLS-сертификата).
При выполнении на клиенте desktop-клиент полностью выполняет вызов локально, а обычный веб-клиент (браузер) делегирует вызов в XMLHttpRequest и поддерживает только то, что предоставляет сам этот API: cookies управляются собственным cookie jar браузера, а таймауты и проверка TLS - самим браузером.
Параметры
Параметры могут передаваться как в строке запроса (для обращения к параметру используется спецсимвол $ и номер этого параметра, начиная с 1), так и в его теле (BODY). В BODY передаются все параметры, не использованные в строке запроса, но только для HTTP-методов с телом (POST, PUT, PATCH, DELETE); для GET параметры, оставшиеся после подстановки в URL, молча отбрасываются.
При обработке параметров файловых классов (FILE, PDFFILE и т.п.) в BODY, тип контента параметра, в зависимости от расширения файла, определяется в соответствии со следующей таблицей. Если расширение файла отсутствует в этой таблице, тип контента устанавливается равным application/<расширение файла>.
Расширение файла при этом определяется автоматически по аналогии с оператором WRITE.
Во всех трех верхних случаях, если значение параметра равняется NULL, то вместо расширения файла в тип контента подставляется null (например application/null), а в качестве самого параметра передается пустая строка.
Параметры классов, отличных от файловых, преобразуются к строкам и передаются с типом контента text/plain. NULL значения передаются как пустые строки.
Собственные заголовки и cookies запроса можно передать вместе с вызовом.
Литеральный текст строки подключения и строки-шаблона тела URL-кодируется перед отправкой запроса (это можно отключить); значения параметров, подставляемых через $N, URL-кодируются независимо.
Результаты
При обработке ответа запроса, результаты с типом контента из следующей таблицы считаются файлами, и могут записываться только в свойства, класс значений которых равен FILE. При этом в расширение файла записывается соответствующее расширение из упомянутой таблицы. Если тип контента отсутствует в этой таблице, но начинается на application, то результат все равно считается файлом, а в расширение этого файла записывается правая часть типа контента (например для типа application/abc в расширение файла записывается abc). Результаты с типом контента application/null считаются равными NULL.
Результаты с типами контента, отличными от вышеупомянутых, считаются строками, и при записи автоматически преобразуются к классам значения свойств, в которые они записываются. Пустые строки при этом преобразуются в NULL.
Заголовки и cookies ответа можно сохранить в свойства. Захваченные cookies объединяют отправленные с запросом и полученные в заголовках Set-Cookie ответа; атрибуты (path, domain и прочие) теряются.
HTTP-код ответа записывается в свойство System.statusHttp[]. Статус, не попадающий в 2xx, также приводит к runtime-исключению с кодом и телом ответа; его можно перехватить оператором TRY, чтобы прочитать System.statusHttp[]. При выполнении на клиенте в обычном веб-клиенте (браузере) сетевая / CORS / DNS-ошибка приходит как status = 0; в этом случае исключение несёт только общее локализованное сообщение об ошибке, а не код + тело ответа.
Несколько результатов / параметров в BODY
Если в BODY передается больше одного параметра, они упаковываются в единое BODY:
- Если задана опция
BODYURL- в качестве BODY передаётся заданная строка, в которой параметры кодируются так, как если бы они передавались в строке запроса, сContent-Type: application/x-www-form-urlencoded. - Иначе - параметры передаются как составные части BODY с
Content-Type: multipart/mixed. ОпцияBODYPARAMNAMESпереключает тип контента наmultipart/form-dataи именует первые части как form-поля; опцияBODYPARAMHEADERSдобавляет дополнительные заголовки к отдельным частям BODY.
Content-Type, заданный вручную через HEADERS, переопределяет умолчание выше: значение multipart/* форсирует multipart-упаковку оставшихся параметров с этим Content-Type; значение не-multipart/* заменяет умолчательный Content-Type у отправляемого BODY.
В свою очередь, если тип контента ответа - multipart/* или application/x-www-form-urlencoded, то тело ответа разбирается на части, и каждая из этих частей считается отдельным результатом выполнения. При этом порядок этих результатов совпадает с порядком соответствующих частей в ответе.
Отметим, что обработка параметров и результатов http запроса во многом аналогична их обработке в обращении из внешней системы по протоколу HTTP (параметры при этом обрабатываются как результаты, и, наоборот, результаты обрабатываются как параметры)
SQL - выполнение команды SQL-сервера.
Для этого типа взаимодействия задается строка подключения и SQL-команда(ы), которую необходимо выполнить. Параметры могут передаваться как в строке подключения, так и в SQL-команде. Для обращения к параметру используется спецсимвол $ и номер этого параметра (начиная с 1). Если выражение SQL-команды заканчивается на .sql, оно трактуется как путь к ресурсу classpath, содержимое которого используется как собственно команда.
EXTERNAL SQL 'LOCAL' не поддерживается; для SQL-запросов к БД, используемой самой платформой, применяется INTERNAL DB.
Параметры
Параметры файловых классов (FILE, PDFFILE и т.п.) можно использовать только в SQL-команде. При этом, если какой-либо из параметров, при выполнении является файлом формата TABLE (TABLEFILE или FILE с расширением table), то такой параметр считается таблицей и в этом случае:
- перед выполнением SQL-команды, значение каждого такого параметра загружается на сервер во временную таблицу
- при подстановке параметров, подставляется не само значение параметра, а имя созданной временной таблицы
Результаты
Результатами выполнения являются: для DML-запросов - числа, равные количеству обработанных записей, для SELECT-запросов - файлы формата TABLE (FILE с расширением table), содержащие результаты этих запросов. При этом порядок этих результатов совпадает с порядком выполнения соответствующих запросов в SQL-команде.
LSF - вызов действия другого lsFusion-сервера
Для этого типа взаимодействия задается строка подключения к lsFusion-серверу (или его веб-серверу, при наличии такового), действие, которые необходимо выполнить, а также список свойств (без параметров), в значения которых будут записаны результаты обращения. Передаваемые параметры должны по количеству и по классам совпадать с параметрами выполняемого действия.
Способ задания действия в этом типе взаимодействия полностью соответствует способу задания действия при обращении из внешней системы.
По умолчанию этот тип взаимодействия реализуется по протоколу HTTP с использованием соответствующих интерфейсов обращений к и из внешней системы.
TCP / UDP - передача байт через сокет
Для этих типов взаимодействия задаётся строка подключения host:port и один параметр файлового класса, байты которого отправляются в сокет. Для TCP платформа выполняет один read() из сокета (в буфер до 10 МБ) и записывает результат в свойство System.responseTcp[]; опциональное свойство System.timeoutTcp[] задаёт таймаут сокета в миллисекундах. UDP отправляет пакет без ожидания ответа.
По умолчанию запрос выполняется на сервере приложений, но может также выполняться с клиента пользователя.
При выполнении на клиенте прямая работа с сокетами доступна локально в desktop-клиенте и через Flutter-мост во Flutter-клиенте (веб/мобильном); обычный веб-клиент (браузер) прямого доступа к сокетам не имеет и падает с UnsupportedOperationException.
DBF - запись строк в .dbf-файл
Для этого типа взаимодействия в качестве строки подключения задаётся путь к .dbf-файлу, а один параметр формата TABLE (TABLEFILE или FILE с расширением table) поставляет строки для записи. Вызов объявляется с ключевым словом APPEND: если файла нет, создаётся новый по схеме входной таблицы; если файл уже существует, он открывается как есть, и строки пишутся в уже имеющиеся в нём поля по именам - эти поля обязаны присутствовать в схеме существующего файла, иначе вызов падает. Опциональная опция CHARSET задаёт кодировку файла (по умолчанию UTF-8).
Имена колонок входной таблицы обрезаются до допустимого в DBF лимита в 10 символов ещё до построения схемы и подстановки в поля; если после обрезания два имени совпадают, вызов падает, а колонка с исходным именем длиннее 10 символов дополнительно теряет свой тип (вместо него используется строковое поле длиной 253) и значение (в файл пишется литеральная строка "null"). Значения NULL во входных ячейках аналогично записываются как литеральная строка "null" - безобидно для строковых полей, но приводит к падению записи для числовых. Поэтому имена колонок входной TABLE стоит заранее привести к виду, совместимому с DBF, а NULL-значения - заменить.
Язык
Для объявления действия, обращающегося к внешней системе, используется оператор EXTERNAL.
Примеры
externalHTTP() {
// GET-запрос с одним файловым результатом
EXTERNAL HTTP GET 'https://www.cs.cmu.edu/~chuck/lennapg/len_std.jpg' TO exportFile;
open(exportFile());
// POST с JSON-телом; фигурные скобки экранируются из-за интернационализации
EXTERNAL HTTP 'http://tryonline.lsfusion.org/exec?action=getExamples' PARAMS JSONFILE('\{"mode"=1\}') TO exportFile;
IMPORT FROM exportFile() FIELDS () TEXT caption, TEXT code DO
MESSAGE 'Example : ' + caption + ', code : ' + code;
}
externalSQL () {
// получаем все штрих-коды товаров с именем мясо
EXPORT TABLE FROM bc=barcode(Article a) WHERE name(a) LIKE '%Мясо%';
// читаем цены для считанных штрих-кодов
EXTERNAL SQL 'jdbc:mysql://$1/test?user=root&password='
EXEC 'select price AS pc, articles.barcode AS brc from $2 x JOIN articles ON x.bc=articles.barcode'
PARAMS 'localhost', exportFile()
TO exportFile;
// для всех товаров с полученными штрих-кодами записываем цены
LOCAL price = INTEGER (INTEGER);
LOCAL barcode = STRING[30] (INTEGER);
IMPORT FROM exportFile() TO price=pc,barcode=brc;
FOR barcode(Article a) = barcode(INTEGER i) DO
price(a) <- price(i);
}
externalLSF() {
EXTERNAL LSF 'http://localhost:7651' EXEC 'System.testAction[]';
}
externalTCP() {
// отправить байты по TCP и прочитать ответ
EXTERNAL TCP 'example.com:9100' PARAMS RAWFILE('payload');
MESSAGE STRING(responseTcp());
}
externalDBF() {
// экспортировать таблицу и дописать строки в .dbf-файл
EXPORT TABLE FROM bc=barcode(Article a), nm=name(a);
EXTERNAL DBF '/tmp/articles.dbf' APPEND PARAMS exportFile();
}