# Сервисы сервера приложений
Сервисы реализованы в опциональном модуле sys, который входит
в дистрибутив Global Server Scala Edition.
Так же модуль может быть подключен в конфигурационном файле сервера
`global3.config.xml`:
~~~xml
~~~
Ленивая загрузка должна быть отключена, для того чтобы сервисы могли всегда принимать запросы.
Более подробную информацию смотрите в руководстве по администрированию сервера.
## SSH консоль сервера
### Введение
Global 3 SE Server включает в себя SSH (Secure Shell) сервер, к которому
возможно подключиться с помощью любого SSH-терминала. С помощью команд
командной строки возможно:
- Смотреть статистику, логи, и управлять клиентскими сессиями
- Перезагружать код прикладных приложений
- Перезапускать Web-приложения
- Выполнять SQL и Jexl-скрипты
### Конфигурирование
Конфигурационный файл global3.config.xml может содержать секцию ``:
```xml
```
- `defaultDb` - Алиас базы данных по умолчанию \
Данное значение используется в
качестве значения параметра по умолчанию для ssh-команды `login`. По
имени базы будет определены имя и пароль пользователя, для подключения к
базе.
- `port` – Порт ssh-сервера \
По умолчанию: 22. \
Данное значение может быть jvm опцией
```
-Dglobal.ssh.port=xxxx
```
или параметрами командной строки при запуске Global3 сервера
```
-global.ssh.port xxxx
```
Учётные данные пользователей, имеющих права на ssh-сединения, указываются в секции `` конфигурационного файл global3.config.xml:
```xml
```
- `user` – Пользователь
- `password` | `encryptedPassword` – Пароль \
Пароль может быть указан как в явном, так и в зашифрованном виде.
- `roles` – Роли \
Список ролей, через запятую, доступных пользователю.
#### Шифрование пароля
Для шифрования пароля необходимо запустить Global 3 Server c парамерами
```
Start.bat -encryptPassword password -masterPassword masterpassword
```
Где:
- `-encryptPassword` \
Пароль который необходимо зашифровать
- `-masterPassword` \
Ключ шифрования, если не указан используется секретный ключ по умолчанию.
Результатом выполнения будет вывод в консоль строки, полученной в
результате шифрования пароля переданного параметром `-encryptPassword`.
### Подключение
Подключиться к SSH-серверу возможно любым SSH-терминалом.
Рекомендуемым клиентом для подключения является [PuTTY](https://www.putty.org).
[Документация по putty](https://putty.org.ru/htmldoc/index.html)
#### Основные параметры
Для подключения необходимо указать:
- *Host Name* \
host или user@host
- *Port* \
22 или пользовательский
- *Connection type* \
SSH
Что бы не вводить значения полей при повторном запуске PuTTY, можно
сохранить параметры подключения по умолчанию, нажав кнопку "Save".
Пример запуска из командной строки:
```
"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin
```
#### Логирование
Для сохранения всего текстового вывода в файл, на вкладке
[Logging](https://putty.org.ru/htmldoc/chapter4.html#config-logging)
диалога подключения необходимо указать:
- *Session Logging* = `All session output`
- *Log file name* = `полный путь к файлу`
```{attention}
В адресах пути:
- Символ `\` необходимо экранировать удвоением \
Пример: `\\`
- Символ `/` экранировать не надо
```
#### Подключение с использованием SSH-RSA ключа
При подключении к SSH из
`*.bat` иил `*.sh` можно воспользоваться RSA-ключами вместо пароля.
```
"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -i ssh_admin_private.ppk
```
Необходимые файлы размещены в подкаталоге `.\ssh`.
Ключи для подключения к пользователю `admin` выдаются по запросу.
#### Запуск терминала
При первом подключении к серверу будет выдано сообщение с
запрашивающие разрешение на подключение к серверу, нажмите: Да.
### Команды
Для получения актуальной справки по доступным командам, выполните команду `help`.
Список команд:
- `alter server mode service|normal` \
Переключает сервер в сервисный режим и обратно.
В сервисном режиме возможно подключение пользовательских сессий
только от имени системного пользователя сервера приложений,
указанного в конфигурационном файле.
```xml
```
- `attach session {sid}` \
Подключение к существующей пользовательской сессии.
- `attach db {dbAlias} [as sys]` \
Подключение к базе данных для выполнения сервисных операций
и/или Jexl-скриптов.
- `сlear` \
Очищает экран терминала.
- `clear persistence cache [{dbAlias}]` \
Очищает Shared-кэш объектов.
- `compare applib {path}` \
Сравнивает jar-файлы в каталоге (или zip-архиве) `{path}`
с jar-файлами в каталоге `SBT.jarFolder`,
соответствующему базе данных, к которой выполнено подключение
командой `attach db`.
Выполняется сравнение версий модулей. \
Выполняется сравнение *.odm.xml файлов на предмет существования новых атрибутов
- `copy applib [force] {path}` \
Выполняет копирование jar-файлов из каталога `{path}` в каталог `SBT.jarFolder`,
соответствующий базе данных, к которой выполнено подключение командой `attach db`.
При указании ключевого слова `force` перед копированием
не выполняется сравнения `*.odm.xml` файлов.
Сравнение версий модулей выполняется в любом случаи.
- `Init schema` \
Выполняет создание/обновление объектов схемы БД
- `execute {expression}` \
Выполняет однострочное Jexl-выражение
- `exit` \
Закрывает SSH-подключение
- `jexl [{file}]` \
Переключает терминал в режим ввода и выполнения
### Выполнение SQL
Для выполнения sql-скрипта:
1. Подключитесь к сессии командами `login`, `attach` или `set sid`.
2. Перейдите в режим ввода скрипта командой `sql`.
3. Введите текст скрипта.
4. Выполните скрипт \
Для этого введите символ `/` с новой строки.
5. Выйдите из режима ввода скрипта \
Для этого введите символ `/` с новой строки.
Пример:
```
login admin/admin@postgres
sql
INSERT INTO gs3_roottest (
id,
idClass
) VALUES (
(select
nextval('GS3_ROOTTEST_SEQ'))
, 12351
) ON CONFLICT DO NOTHING;
/
/
```
#### Выполнение SQL скрипта из файла
Для выполнения sql-скрипта из файла:
1. Подключитесь к сессии.
2. Выполните команду `sql {file}`. \
Где `{file}` – путь к файлу к файлу на сервере.
```{attention}
Файл должен находится на локальном диске сервера.
```
Пример:
```
login admin/admin@postgres
sql D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_sql_exams.txt
```
Содержимое файла:
```sql
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
/
```
### Jexl скрипты
#### Выполнение Jexl скрипта
Для выполнения Jexl-скрипта необходимо:
1. Подключитесь к сессии
2. Переключитесь в режим ввода скрипта командой `jexl`.
3. Введите текст скрипта.
4. Выполните скрипт \
Для этого введите символ `/` с новой строки.
5. Выйдите из режима ввода \
Для этого введите символ `/` с новой
строки.
Пример 1:
```
login admin/admin@postgres
jexl
var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/
```
Пример 2 (для dataInstall):
```
login admin/admin@postgres
jexl
Bbb_DBTypeApi.dataInstall();
Btk_Pkg.commit();
/
/
```
Пример 3:
```
login admin/admin@postgres
jexl
Btk_Pkg.setRWSharedUOWEditType();
Prs_EntTransApi.dataInstall();
Btk_Pkg.commit();
/
/
```
#### Выполнение Jexl скрипта из файла
Для выполнения Jexl-скрипта из файла необходимо:
1. Подключитесь к сессии
2. Выполнить команду jexl `{file}` \
Где `{file}` – путь к файлу к файлу на
сервере.
```{attention}
Файл должен находится на локальном диске сервера.
```
Пример:
```
login
jexl D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_jexl_exams.txt
```
Содержимое файла:
```
var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/
```
#### Контекст выполнения Jexl скрипта
Возможны следующие контексты выполнения Jexl-скрипта.
##### Пользовательская сессия
```
>login user/password@alias
>jexl
jexl>
/
/
>
```
В данном контексте доступны все Api-классы, присутствующие в SBT,
соответсвующего пользовательской сессии.
##### База данных
```
>attach db alias
>jexl
jexldb>
/
/
>
```
Доступны методы управления инстансом базы данных:
- `initschema()` \
Выполняет инициализацию/обновление схемы БД
в соответствии с текущей прикладной кодовой базой.
##### Системный контекст
```
>attach db alias as sys
>jexl
jexlsys>
/
/
>
```
Данный контекст является административным и предназначен для управления
сервером приложений. Доступны следующие методы:
- `upgrade({release_path})` \
Выполняет инициализацию/обновление схемы БД в соответствии с текущей прикладной кодовой базой.\
Где: \
`{release_path}` – путь к каталогу или zip-архиву с релизом прикладных модулей
### Выполнение командного файла ssh
Для выполнения командного файла из командной строки можно выполнить:
```
"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin -m shh_logger_test_script.txt
```
```{attention}
В данном примере файл shh_logger_test_script.txt размещён на диске
клиентской машины, он считывается в момент подключения к SSH-серверу.
Пути к файлам в скрипте, являются локальными для сервера.
```
Содержимое файла:
```
login
log-info app el
execute Btk_ClassApi.getCanonicalClassName("Btk_Object")
jexl
var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/
jexl D:/svn/depot/ASSource/sysapplication/ssh/src/test/java/ru/bitec/app/ssh/shh_jexl_exams.txt
jexl D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_jexl_exams.txt
sql
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
/
sql D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_sql_exams.txt
log-off all
logout
```
#### Логирование
При выполнении скрипта, по умолчанию, в ssh-консоль выводится результат
выполнения команды или сообщение об ошибке со стеком вызова.
Для вывода в ssh-консоль дополнительных логов, необходимо их включить.
В консоль можно вывести логи следующих типов:
- `oper` \
Логи из классов с namespace `ru.bitec.engine.model.operation`.
- `sql` \
Логи sql-вызовов с уровня jdbc-соединения с базой.
- `script` \
Логи из скриптового языка в режиме совместимости с Global 1.
При работе со Scala не имеют смысла.
- `app` \
Логи из классов прикладной логики с namespace `ru.bitec.app.*`.\
Пример отправки сообщения:
```scala
Logger.Factory.get(Xxx_XxxApi.class).info("Текст сообщения")
```
- `el` \
Логи из инфраструктуры EclipseLink. В основном, это sql-вызовы, в
более компактном виде, чем логи jdbc.
- `all` \
Все выше перечисленные типы логов.
Для переключения уровней логирования используются команды:
- `log-off`
- `log-error`
- `log-warn`
- `log-info`
- `log-debug`
- `log-trace`
Команды указаны в порядке уменьшения уровня логов.
В результате выполнения этой команды:
```
log-info app el
```
в ssh-лог будут попадать сообщения типов `app` и `el` для уровней
логирования: `error`, `warn`, `info`.
Для отключения вывода сообщений в ssh-лог необходимо вызвать команду:
```
log-off all
```
##### Перенаправление ssh-лога Putty в файл
При выполнении скриптов из командной строки, часто необходимо
перенаправить вывод в файл. Для ssh-консоли Putty, это выполняется
передачей параметра `-sessionlog {имя_файла.txt}`
```
"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin -m shh_logger_test_script.txt -sessionlog sessionlog.txt
```
### FAQ
#### Зависает ssh-терминал на этапе подключения к серверу.
При запуске Putty из скрипта при первом подключении
к серверу выдается сообщение PuTTY Security Alert, что может
приводить к зависанию скрипта.
Необходимо дополнительно передать подтверждение при запуске:
```
echo yes | %plink% -ssh localhost -P 2222 -l admin -i ssh_admin_private.ppk
```
```{note}
Plink также входит в состав дистрибутива PuTTY.
```
## WebSocket консоль сервера
### Введение
Global 3 SE Server предоставляет консоль управления, доступную через WebSocket соединение.
С помощью команд передаваемых через WebSocket возможно:
- Перезагружать код прикладных приложений
- Выполнять Jexl-скрипты
### Алгоритм работы с консолью
- Открытие WebSocket-соединения
- Отправка команды аутентификации пользователя в системе
- Отправка исполняемых команд
- Закрытие WebSocket-соединения
### Открытие WebSocket соединения
Для окрытия WebSocket соединения с консолью сервера, необходимо выпонить http-запрос по адресу: `ws[s]://{server[:port]}/app/sys/ws/console`.
### Формат команды
Командой является строка в формате:
`{command}[\n{arguments}]`
где:
`{command}` - строка команды. Может состоять из одного или нескольких слов.
`\n` - символ новой строки #10. Является разделителем команды и её аргументов. Не обязателен, если у команды нет аргуметтов.
`{arguments}` - строка аргументов команды. Может содержать любые символы, включая перносы строк.
### Формат результата выполнения команды
Результатом выполения команды является строка в формате JSON:
```json
{
"success":true,
"data": null,
"exception":null,
"exceptionStack":null
}
```
где:
`success` - Флаг успешности выполнения команды: `true|false`
`data` - Результат выполненения команды: `null|"string"|JSON`
`exception` - Сообщение возникшего исключения: `null|"string"`
`exceptionStack` - Стек возникшего исключения: `null|"string"`
### Список команд
- `login`\n
`{user}/{password}@{database}`
Выполняет аутентификацию пользователя `user` в базе данных `database` и запускает рабочий сеанс пользователя.
*Использование команды `login`, в качестве способа уатентификации, было выбрано по причине невозможности использования http-заголовков некоторыми WebSocket-клиентами. Например: [JMeter WebSocket Load Testing Sampler](https://www.blazemeter.com/blog/jmeter-websocket-samplers).*
- `logout`
Закрывает рабочий сеанс пользователя.
- `reload sbt`
Выполняет перезагрузку прикладного кода текущего решения.
- `reload sbt force`
Выполняет перезагрузку инфраструктуры EclipseLink, прикладного кода и общих библиотек текущего решения.
- `jexl`\n
`// Произвольный текст Jexl-скрипта`
`return 1 + 2;`
*Любое значение, возвращённое из Jexl-скрипта, будет преобразовано в строку.*
### Java пример взаимодествия с консолью
Библиотеки, используемые в примере клиента:
- "org.asynchttpclient" % "async-http-client" % "2.12.3"
- "com.fasterxml.jackson.core" % "jackson-databind" % "2.8.9"
``` java
package ru.bitec.app.examples.ws.console;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.Dsl;
import org.asynchttpclient.ws.WebSocket;
import org.asynchttpclient.ws.WebSocketListener;
import org.asynchttpclient.ws.WebSocketUpgradeHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.websocket.CloseReason;
import java.io.Serializable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class WsConsoleClientTest {
public static void main(String[] args) throws Exception {
try (WsConsoleClient client = WsConsoleClient.open("ws://localhost:8080/app/sys/ws/console")) {
client.login("admin", "admin", "pgtest");
System.out.println("1 + 2 = " + client.evaluateJexl("return 1 + 2;"));
client.reloadSbt(false);
client.logout();
}
}
public class WsConsoleClient implements AutoCloseable {
public static WsConsoleClient open(String url) throws Exception {
return open(url, 3000);
}
public static WsConsoleClient open(String url, int timeout) throws Exception {
return new WsConsoleClient().connect(url, timeout);
}
private final ObjectMapper objectMapper_ = new ObjectMapper();
private int commandTimeout_ = 60000;
private AsyncHttpClient asyncHttpClient_;
private WebSocket webSocketClient_;
private volatile CompletableFuture webSocketResultFuture_;
private final WebSocketUpgradeHandler wsHandler = new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() {
@Override
public void onOpen(WebSocket websocket) {
// WebSocket connection opened
}
@Override
public void onClose(WebSocket websocket, int code, String reason) {
if (code != CloseReason.CloseCodes.NORMAL_CLOSURE.getCode()) {
webSocketResultFuture_.completeExceptionally(new Exception(reason));
}
}
@Override
public void onError(Throwable t) {
webSocketResultFuture_.completeExceptionally(t);
}
@Override
public void onTextFrame(String payload, boolean finalFragment, int rsv) {
webSocketResultFuture_.complete(payload);
}
}).build();
public WsConsoleClient connect(String url, int timeout) throws Exception {
AsyncHttpClient syncHttpClient = Dsl.asyncHttpClient();
try {
webSocketClient_ = syncHttpClient
.prepareGet(url)
.setRequestTimeout(timeout)
.execute(wsHandler)
.get();
} catch (Exception e) {
syncHttpClient.close();
throw e;
}
asyncHttpClient_ = syncHttpClient;
return this;
}
public void close() throws Exception {
if (asyncHttpClient_ != null) {
try {
if (webSocketClient_ != null && webSocketClient_.isOpen()) {
try {
webSocketClient_.sendCloseFrame().get();
} finally {
webSocketClient_ = null;
}
}
} finally {
asyncHttpClient_.close();
}
}
}
public int getCommandTimeout() {
return commandTimeout_;
}
public void setCommandTimeout(int commandTimeout) {
this.commandTimeout_ = commandTimeout;
}
public void login(String user, String password, String database) throws Exception {
sendCommand("login", String.format("%s/%s@%s", user, password, database));
}
public void logout() throws Exception {
sendCommand("logout");
}
public String evaluateJexl(String script) throws Exception {
return sendCommand("jexl", script);
}
public void reloadSbt(boolean force) throws Exception {
sendCommand("reload sbt" + (force ? " force" : ""));
}
String sendCommand(String command) throws Exception {
return sendCommand(command, null);
}
String sendCommand(String command, String args) throws Exception {
validateConnection();
if (command == null || command.trim().isEmpty()) {
throw new Exception("Command can not be null or empty.");
}
webSocketResultFuture_ = new CompletableFuture<>();
try {
String payload = command + "\n" + (args != null ? args : "");
webSocketClient_.sendTextFrame(payload).get();
} catch (Exception e) {
webSocketResultFuture_.completeExceptionally(e);
}
return parseResponse(webSocketResultFuture_.get(commandTimeout_, TimeUnit.MILLISECONDS));
}
private String parseResponse(String response) throws Exception {
ConsoleResponse consoleResponse = objectMapper_.readValue(response, ConsoleResponse.class);
if (consoleResponse.success) {
return consoleResponse.data;
} else {
throw new Exception(consoleResponse.exception);
}
}
private void validateConnection() throws Exception {
if (webSocketClient_ == null) {
throw new Exception("WebSocket connection is not connected.");
}
if (!webSocketClient_.isOpen()) {
throw new Exception("WebSocket connection is closed");
}
CompletableFuture webSocketResultFuture = webSocketResultFuture_;
if (webSocketResultFuture != null && !webSocketResultFuture.isDone()) {
throw new Exception("Prior console command is not completed. Wait for the result of the previous command.");
}
}
private final static class ConsoleResponse implements Serializable {
private static final long serialVersionUID = -5577579081118070434L;
/**
* Флаг успешного выполнения запроса. false, если при обработке запроса возникло исключение.
*/
private boolean success = false;
/**
* Строковые данные
*/
private String data;
/**
* Текст возникшего исключения
*/
private String exception;
/**
* Стек возникшего исключения
*/
private String exceptionStack;
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getException() {
return exception;
}
public void setException(String exception) {
this.exception = exception;
}
public String getExceptionStack() {
return exceptionStack;
}
public void setExceptionStack(String exceptionStack) {
this.exceptionStack = exceptionStack;
}
}
}
}
```
## Jexl через SOAP XML
### Введение
В Global 3 SE Server реализован SOAP XML сервис, позволяющий выполнять
Jexl-скрипт в контексте пользовательской сессии.
### Аутентификация
Читайте в разделе [Аутентификация в REST/SOAP-сервисах](#аутентификация-в-restsoap-сервисах).
### Схемы
WSDL описание сервиса доступно по адресу:
```
http://{server:port}/app/sys/soap/sys-service-1.0.0?wsdl
```
XSD схемы POJO запроса и ответа доступны по адресу:
```
http://{server:port}/app/sys/soap/sys-service-1.0.0?xsd=1
```
### Передача бинарных данных с использованием MTOM
Сервис поддерживает оптимизированную передачу бинарных данных в сервис и
обратно, через поля attachment объектов `JexlSoapRequest` и
`JexlSoapResponse`.
### Доступ к данным SOAP-запроса из прикладного кода
В прикладном коде Api-/Pkg-классов, выполняющемся в результате вызова
Jexl-скрипта из SOAP-сервиса, доступен объект `SoapContext`. `SoapContext`
предоставляет доступ к данным SOAP-запроса и SOAP-ответа. Для получения
контекста, выполните:
```scala
val soapContextOpt = SoapContext()
```
Методы доступные в `SoapContext`:
- `hasInputAttachment: Boolean` \
Указывает на наличие у SOAP-запроса прикреплённых с использованием MTOM бинарных данных
- `getInputData: String` \
Возвращает строковые данные SOAP-запроса.
- `forInputStream(foo: InputStream => Unit): Unit` \
Предоставляет доступ к прикреплённым бинарным данным
- `setOutputData(data: String): Unit` \
Устанавливает строковые данные в SOAP-ответ.
- `forOutputStream(foo: OutputStream => Unit): Unit` \
Предоставляет доступ к выходному потоку бинарных данных передаваемых средствами MTOM
### Методы обработки данных SOAP-запроса из Jexl-скрипта
В пакете `Gtk_SoapPkg` хранятся методы, использующие `SoapContext`:
- `setOutputData(value: AnyRef): Unit` \
Установливает вывод результата выполнения soap-запроса.
- `inputStreamToTemp: File` \
Создает временный файл с данными из прикреплённых данных soap-запроса
- `attachFile(file: File): Unit` \
Принимает файл, закачаивает его данные в выходной поток бинарных данных soap-запроса
Пример ипользования из Jexl-скрипта:
```
var f = new("java.io.File", 'C:\users\userName\data.txt');
Gtk_SoapPkg.setOutputData('Данные в прикрепленном файле');
Gtk_SoapPkg.attachFile(f);
```
Данный скрипт, отправленный через SOAP-запрос, в ответ запишет
- в data - строку 'Данные в прикрепленном файле'
- в attachment - закодированный base64 файл
## REST сервис c обработкой http-запроса в прикладном пакете
### Введение
В Global 3 SE Server реализован REST-сервис, позволяющий выполнять
обработку HTTP-запроса в прикладном пакете, в контексте прикладной
сессии.
Возможны 2 режима обработки запросов:
- Exclusive Session - с сохранением состояния между запросами. Rest- и
Gtk-сессии создаются при первом обращении клинета к сервису и
закрываются по таймауту или при явном указании на необходимость
закрытия сессий по завершению запроса.
- Shared Session - без сохранения состояния между запросами. Каждый
запрос обрабатывается в новых Rest- и Gtk-сессиях. Доступно с AS 1.14
RC7.
Пакет должен быть унаследован от одного из трейтов `RestPkg`, `RestESPkg`,
`RestSSPkg`.
```scala
class Xxx_XxxPkg extends Pkg with RestPkg {
}
object Xxx_XxxPkg extends PkgFactory[Xxx_XxxPkg]{
}
```
### Аутентификация
Читайте в разделе [Аутентификация в REST/SOAP-сервисах](#аутентификация-в-restsoap-сервисах).
### Методы `RestPkg` / `RestESPkg` / `RestSSPkg`
- `get(relativePath: String): AnyRef` \
Вызывается при поступлении GET запроса. \
Допускается возврат: `null`, `None`, `String`, `ResponseBuilder`.
- `post(relativePath: String): AnyRef` \
Вызывается при поступлении POST запроса. \
Допускается возврат: `null`, `None`, `String`, `ResponseBuilder`.
- `onActivate()` \
Вызывается при подключении новой gtk-сессии
- `onDeactivate()` \
Вызывается при закрытии gtk-сессии
- `beforeReload(keyBundle: KeyBundle)` \
Вызывается перед перезагрузкой SBT
- `afterReload(keyBundle: KeyBundle)` \
Вызывается после перезагрузки SBT и пересоздании gtk-сессии
- `isSupportsSharedSession(): Boolean `\
Метод указывает, что пакет может обрабатывать запросы в режиме разделяемой GTK-сессии.
- `isSupportsExclusiveSession(): Boolean` \
Метод указывает, что пакет может обрабатывать запросы в режиме эксклюзивной GTK-сессии.
#### Допустимые результаты методов get() и post()
От типа возвращённого значения будет зависеть способ формирования ответа
на Http-запрос.
- `null`,`None` \
Ответ будет сформирован через `RestfulContext().get.responseBuilder`
- `String` \
В ответ будет записано возвращённая строка
- `ResponseBuilder` \
Ответ будет сформирован через возвращенный `ResponseBuilder`.
Он может быть равен `RestfulContext().get.responseBuilder`, либо сформирован
произвольным образом.
- `Response` \
Ответом будет возвращенное значение.
### Адреса
Доступно с AS 1.14 RC7:
- `http://{server:port}/app/sys/rest/es/pkg/{Xxx_XxxxPkg}/{relativePath}`
Доступно с AS 1.14 RC7:
- `http://{server:port}/app/sys/rest/ss/pkg/{Xxx_XxxxPkg}/{relativePath}`
где:
- `{Xxx_XxxxPkg}` \
Имя прикладного пакета, унаследованного от RestfulPkg
- `{relativePath}` \
Произвольный путь, который будет передан в методы
get/post прикладного пакета
- `http://{server:port} ` \
Адрес подключения к серверу
- `sys` \
Системный прикладной модуль, в будущем будет app/sys
- `rest` \
Шлюз для rest запросов
- `es` / `ss`\
Группировка по времени жизни gtk-сессии (Exclusive Session) / (Shared
Session)
- `pkg`
Узел для доступа к пакетам.
### Рабочее пространство
`Workspace` - Рабочее пространство.
Используется для возможности параллельной работы
нескольких gtk сессий в рамках одного пользователя в es режиме.
Для предотвращения неконтролируемого разрастания сессий, количество сессий на пользователя в
эксклюзивном режиме ограничено. На одного пользователя и один workspace может существовать только
одна сессия.
Рабочее пространство задается в http заголовке:
- `Workspace` \
Имя рабочего пространства пользователя.
### Exclusive Session
#### Принцип работы сервиса
1. Получение HTTP-запроса (GET или POST)
2. Проверка авторизационных данных пользователя.
3. Получение, захват существующего рабочего сеанса или создание нового.
4. Вызов метода прикладного пакета `get(…)` или `post(…)`, соответственно \
В данном методе производится формирование тела http-ответа
5. Отправка ответа
#### Жизненный цикл http-сессий
При обращении к rest - сервису.
1. Если cookie `JSESSIONID` не задан, создается новая http сессия
2. Иначе, используется сессия с переданным идентификатором \
Если переданный id не корректный создается новая http сессия
3. Обработка запроса
4. Возврат результата \
При этом cookie `JSESSIONID` будет содержать идентификатор http сессии
Время жизни http-сессии 15 минут. При отсутствии обращений к http-сессии
в течении этого интервала, сессия уничтожается, и установленные в её
атрибуты значения становятся не доступны.
#### Жизненный цикл gtk-сессий
Gtk-сессия существует в разрезе 4-х параметров:
1. Алиас база данных \
Получается из http-заголовка `Database`. Если
заголовок не передан, используется алиас из конфигурационного файла
сервера.
2. Имя пользователя \
Получается из авторизационных данных. Передаётся в
http-заголовке `Authorization`
3. Имя прикладного пакета \
Получается из строки адреса
4. Рабочее пространство \
Получается из http-заголовка `Workspace`. Если
не задано, используется `Default`
Одной http-сессии (одному `JSESSIONID`) может соответствовать только одна
gtk-сессия. При обращении к другому пакету из одной http-сессии,
предыдущая gtk-сессия будет деактивирована и закрыта.
Таймаут gtk-сессии по умолчанию – 15 минут. Не используемая gtk-сессия
будет закрыта через 15 минут. Изменить таймаут или закрыть сессию по
завершению обработки запроса возможно через установку свойств
`RestEXContext()`.
##### Создание новой сессии
Происходит в случае, если:
- в запрос не передан куки http сессии
- или gtk сессия не существует в уникальном разрезе
- Имя пользователя
- База данных
- Workspace
- Пакет
При этом:
- Создается gtk сессия
- Вызываются методы прикладного пакета
- `onActivate`
- в соответствии с типом http запроса вызывается один из методов
обработки запроса
- `get`
- `post`
- В ответ добавляется кука http сессии
##### Работа в текущий сессии
Происходит в случае, если:
- в запрос передан куки http сессии
- и gtk сессия существует в уникальном разрезе, и ее куки совпадает с
переданным запросом
##### Таймаут сессии
Происходит в случае, если:
- в запрос передан куки не существующий http сессии
- gtk сессии не существует
При этом
- Происходит создание новой сессии и вызов метода обработки (смотри
создание)
- Возврат новой куки
##### Конфликт сессии
Происходит в случае, если:
- в запрос передан куки http сессии
- gtk сессия существует в уникальном разрезе, и ее текущий куки не
совпадает с куки http сессии
При этом
- Генерируется ошибка захвата сессии
##### Захват сессии
Происходит при условии:
- в запрос не передан куки http сессии
- или передан устаревший куки, который совпадает с куки в gtk сессии
- gtk сессия существует в уникальном разрезе
При этом:
- Вызываются методы прикладного пакета
- `onDeactivate`
- `onActivate`
- в соответствии с типом http запроса вызывается один из методов
обработки
- Возвращается куки новой сессии
#### Доступ к данным REST-запроса из прикладного кода
В прикладном коде Pkg-класса, выполняемом при REST-запросе, доступен
объект `RestESContext`, предоставляющий доступ к данным REST-запроса и
REST-ответа. Для получения контекста, выполните:
```scala
val restContextOpt = RestESContext()
```
Методы контекста:
- `sessionTimeout: Long` \
Время жизни gtk-сессии после последнего запроса
- `request: HttpServletRequest` \
Объект-запрос. Предоставляет доступ к параметрам, кукам и т.д.
- `responseBuilder: Response.ResponseBuilder` \
Билдер ответа.
- `markSessionClose(): Unit` \
Устанавливает флаг закрытия gtk-сессии по завершении обработки запроса
- `isSessionClose: Boolean` \
Возвращает значение флага закрытия gtk-сессии
### Обработка ошибок
#### Коды ошибок
- `500` – server error \
Прикладное исключение
- `409` - conflict \
Конфликт сессий
#### Формат ошибок
При возникновении ошибки, по умолчанию, ответ будет возвращён в формате
xml.
Для изменения формата ответа, передайте http-параметр `format`.
Доступные значения:
- `xml`
- `json`
Пример:
```
http://localhost:8080/app/sys/rest/es/pkg/Gs3_RestfulTestPkg/anypath?format=json
```
##### Xml ответ с ошибкой
```xml
\
%s
%s
%s
```
##### Json ответ с ошибкой
```json
{
"response": {
"status" : 0,
"data":{},
"error": {
"type": "" ,
"message": "",
"stacktrace": ""
}
}}
```
### Пример обращения к сервису
#### Java
```java
package ru.bitec.app.sys.rest;
import org.junit.Test;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PkgRestServiceTest0 {
private static String rootAddress = "http://localhost:8080/sys/rest/es";
private static Client client = ClientBuilder.newClient();
private Path testPath = Paths.get(System.getProperty("user.dir"), "src\\test\\java\\ru\\bitec\\app\\sys\\rest");
private Path cookiePath = testPath.resolve("JSESSIONID.cookie");
@Test
public void get_2_Cookie_test() throws Exception {
String JSESSION_Cookie = load_JSESSIONID_Cookie();
Invocation.Builder builder = client.target(rootAddress + "/pkg/Gs3_RestfulTestPkg/anypath").request()
//.header("Authorization", "Basic " + Base64.encodeToString("admin:admin".getBytes("UTF-8"), false))
.header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")))
.cookie(Cookie.valueOf(JSESSION_Cookie));
Response response = builder.get();
try {
save_JSESSIONID_Cookie(response);
String s = response.readEntity(String.class);
System.out.println("response: " + s);
} finally {
response.close();
}
}
private void save_JSESSIONID_Cookie(Response response) throws IOException {
if (response.getCookies().containsKey("JSESSIONID")) {
String cookie = response.getCookies().get("JSESSIONID").toString();
Files.write(cookiePath, cookie.getBytes(Charset.defaultCharset()));
}
}
private String load_JSESSIONID_Cookie() throws IOException {
if (Files.exists(cookiePath))
return new String(Files.readAllBytes(cookiePath), Charset.defaultCharset());
else
return null;
}
}
```
## REST-сервис для взаимодействия с пользовательскими сессиями
Сервис доступен только в режиме разработки. В проектных решениях не
доступен.
Функциональность сервиса:
1. Получение списка сессий, удовлетворяющих условиям поиска.
2. Выполнение Jexl-скриптов в контексте гл.выборки приложения.
### Получение списка сессий
При отправке HTTP GET на адрес сервиса, будет возвращён JSON со списком
сессий, удовлетворяющих условиям поиска. Http-запрос может содержать
следующие параметры:
- `sbt` \
Фильтр по имени SBT
- `clientId` \
Фильтр по идентификатору клиента (этот идентификатор
присваивается каждому экземпляру браузера и содержится в куках)
- `user` \
Фильтр по имени пользователя
- `app` \
Фильтр по имени главной выборки приложения (поиск
осуществляется по вхождению переданного значения в полное имя
гл.выбокрки)
Пример HTTP GET:
```
http://localhost:8080/app/sys/rest/sessions?sbt=test&user=admin&clientId=123456&app=Xxx_xxxxxx
```
Пример ответа:
```json
{"sessions":[{
"sid": "E1",
"id": "02b5f602-ac2c-4259-9452-18840a1cd124",
"user": "admin",
"clientId": "B54E2C1F-04E5-4BB2-9372-A9AB8E9C6676",
"database": "PGTEST",
"app": "gtk-ru.bitec.app.gs3.Gs3_TestXmlApplication"
}]}
```
### Выполнение Jexl в контексте главной выборки приложения
#### Простой HTTP POST
При отправке HTTP POST на адрес сервиса, в контексте гл.выборки
приложения сессии, с переданным идентификатором, будет выполнен
Jexl-скрипт, переданный в теле Http POST.
Адреса сервисов:
- `http://{server:port}/app/sys/rest/sessions/{id}/jexl/mainsel` \
Где `{id}` - Идентификатор сессии. Значение `{id}` можно получить из
результата http get к сервису.
- `http://{server:port}/app/sys/rest/sessions/new/jexl/mainsel?appname={имя_гл_выборки}` \
При этом будет запущена новая сессия.
Для корректного запуска новой сессии и открытия приложения,
необходимо передать имя главной выборки через http-параметр `appname`
Пример HTTP POST:
```
http://localhost:8080/app/sys/rest/sessions/949d77e9-80bf-4b93-bd3b-b424f8887783/jexl/mainsel
http://localhost:8080/app/sys/rest/sessions/new/jexl/mainsel?appname=Gs3_TestXmlApplication
```
В ответе вернётся JSON с результатом выполнения:
```json
{
"sessionId":"14849330-3196-4b33-80c5-caa370186720",
"result":"[результат выполнения Jexl]"
}
```
##### Пример на Java
```java
@Test
public void post_test_1() throws Exception {
Map rootMap = (Map) Json.parse(doTestGET(rootAddress + "?sbt=test"));
List