REST-сервис#
REST-сервис с обработкой HTTP-запроса в прикладном пакете#
В Global 3 SE Server реализован REST-сервис, позволяющий выполнять обработку HTTP-запроса в прикладном пакете в контексте прикладной сессии.
Возможны два режима обработки запросов:
Exclusive Session — с сохранением состояния между запросами. Rest- и Gtk-сессии создаются при первом обращении клиента к сервису и закрываются по таймауту или при явном указании на необходимость закрытия сессий по завершению запроса.
Shared Session — без сохранения состояния между запросами. Каждый запрос обрабатывается в новых Rest- и Gtk-сессиях. Доступно с AS 1.14 RC7.
Пакет должен быть унаследован от одного из трейтов RestPkg, RestESPkg, RestSSPkg.
class Xxx_XxxPkg extends Pkg with RestPkg {
}
object Xxx_XxxPkg extends PkgFactory[Xxx_XxxPkg] {
}
Аутентификация#
Читайте в разделе Аутентификация в REST/SOAP-сервисах.
Методы 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}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-сессий в рамках одного пользователя в режиме Exclusive Session.
Для предотвращения неконтролируемого разрастания сессий количество сессий на пользователя в эксклюзивном режиме ограничено. На одного пользователя и один workspace может существовать только одна сессия.
Рабочее пространство задаётся в HTTP-заголовке:
Workspace — имя рабочего пространства пользователя.
Exclusive Session#
Принцип работы сервиса#
Получение HTTP-запроса (GET или POST).
Проверка авторизационных данных пользователя.
Получение, захват существующего рабочего сеанса или создание нового.
Вызов метода прикладного пакета
get(…)илиpost(…), соответственно.
В данном методе производится формирование тела HTTP-ответа.Отправка ответа.
Жизненный цикл HTTP-сессий#
При обращении к REST-сервису:
Если cookie
JSESSIONIDне задан, создаётся новая HTTP-сессия.Иначе используется сессия с переданным идентификатором.
Если переданный ID некорректен, создаётся новая HTTP-сессия.Обработка запроса.
Возврат результата.
При этом cookieJSESSIONIDбудет содержать идентификатор HTTP-сессии.
Время жизни HTTP-сессии — 15 минут. При отсутствии обращений к HTTP-сессии в течение этого интервала сессия уничтожается, и установленные в её атрибуты значения становятся недоступны.
Жизненный цикл Gtk-сессий#
Gtk-сессия существует в разрезе четырёх параметров:
Алиас базы данных — получается из HTTP-заголовка
Database. Если заголовок не передан, используется алиас из конфигурационного файла сервера.Имя пользователя — получается из авторизационных данных, передаётся в HTTP-заголовке
Authorization.Имя прикладного пакета — получается из строки адреса.
Рабочее пространство — получается из HTTP-заголовка
Workspace. Если не задано, используетсяDefault.
Одной HTTP-сессии (одному JSESSIONID) может соответствовать только одна Gtk-сессия. При обращении к другому пакету из одной HTTP-сессии предыдущая Gtk-сессия будет деактивирована и закрыта.
Таймаут Gtk-сессии по умолчанию — 15 минут. Неиспользуемая Gtk-сессия будет закрыта через 15 минут. Изменить таймаут или закрыть сессию по завершению обработки запроса возможно через установку свойств RestEXContext().
Создание новой сессии#
Происходит в случае, если:
в запрос не передан cookie HTTP-сессии;
или Gtk-сессия не существует в уникальном разрезе:
Имя пользователя;
База данных;
Workspace;
Пакет.
При этом:
Создаётся Gtk-сессия.
Вызываются методы прикладного пакета:
onActivate();в соответствии с типом HTTP-запроса — один из методов обработки запроса:
get();post().
В ответ добавляется cookie HTTP-сессии.
Работа в текущей сессии#
Происходит в случае, если:
в запрос передан cookie HTTP-сессии;
и Gtk-сессия существует в уникальном разрезе, и её cookie совпадает с переданным в запросе.
Таймаут сессии#
Происходит в случае, если:
в запрос передан cookie несуществующей HTTP-сессии;
Gtk-сессия не существует.
При этом:
Происходит создание новой сессии и вызов метода обработки (см. «Создание»).
Возвращается новый cookie.
Конфликт сессии#
Происходит в случае, если:
в запрос передан cookie HTTP-сессии;
Gtk-сессия существует в уникальном разрезе, и её текущий cookie не совпадает с cookie HTTP-сессии.
При этом:
Генерируется ошибка захвата сессии.
Захват сессии#
Происходит при условии:
в запрос не передан cookie HTTP-сессии;
или передан устаревший cookie, который совпадает с cookie в Gtk-сессии;
Gtk-сессия существует в уникальном разрезе.
При этом:
Вызываются методы прикладного пакета:
onDeactivate();onActivate().
В соответствии с типом HTTP-запроса вызывается один из методов обработки.
Возвращается cookie новой сессии.
Доступ к данным REST-запроса из прикладного кода#
В прикладном коде Pkg-класса, выполняемом при REST-запросе, доступен объект RestESContext, предоставляющий доступ к данным REST-запроса и REST-ответа.
Для получения контекста выполните:
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 version="1.0" encoding="UTF-8"?>
<response>
<status>%s</status>
<error>
<type>%s</type>
<message>%s</message>
<stacktrace><![CDATA[%s]]></stacktrace>
</error>
</response>
JSON-ответ с ошибкой
{
"response": {
"status": 0,
"data": {},
"error": {
"type": "",
"message": "",
"stacktrace": ""
}
}
}
Пример обращения к сервису#
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 " + 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-сервис для взаимодействия с пользовательскими сессиями#
Сервис доступен только в режиме разработки. В проектных решениях недоступен.
Функциональность сервиса:
Получение списка сессий, удовлетворяющих условиям поиска.
Выполнение 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
Пример ответа:
{
"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 с результатом выполнения:
{
"sessionId": "14849330-3196-4b33-80c5-caa370186720",
"result": "[результат выполнения Jexl]"
}
Пример на Java
@Test
public void post_test_1() throws Exception {
Map<String, Object> rootMap = (Map<String, Object>) Json.parse(doTestGET(rootAddress + "?sbt=test"));
List<Map<String, Object>> sessions = (List<Map<String, Object>>) rootMap.get("sessions");
if (sessions.isEmpty())
throw new Exception("No one ESession opened.");
Map<String, Object> session = sessions.get(0);
String id = (String) session.get("id");
String rootAddress = "http://localhost:8080/app/sys/rest/sessions";
doTestPOST(rootAddress + "/" + id + "/jexl/mainsel", "Some Jexl Script");
}
public String doTestGET(String uri, String database) throws Exception {
System.out.println("HTTP GET: " + uri);
Invocation.Builder builder = client.target(uri).request()
.header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")));
if (database != null) {
builder.header("Database", database);
}
String result = "";
Response response = builder.get();
try {
System.out.println("response status = " + response.getStatus());
System.out.println("response MediaType = " + response.getMediaType());
if (response.getStatus() == 200) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream inputStream = response.readEntity(InputStream.class)) {
StreamHelper.copyStream(inputStream, out);
}
byte[] bytes = out.toByteArray();
System.out.println("response: " + bytes.length + " bytes");
result = new String(bytes);
} else {
result = response.readEntity(String.class);
}
} finally {
response.close();
}
System.out.println(result);
return result;
}
public String doTestPOST(String uri, String database, String body) throws Exception {
System.out.println("HTTP POST: " + uri);
Invocation.Builder builder = client.target(uri).request()
.header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")));
if (database != null) {
builder.header("Database", database);
}
String result = "";
Entity entity = Entity.entity(body, MediaType.TEXT_PLAIN_TYPE);
Response response = builder.post(entity);
try {
System.out.println("response status = " + response.getStatus());
System.out.println("response MediaType = " + response.getMediaType());
if (response.getStatus() == 200) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (InputStream inputStream = response.readEntity(InputStream.class)) {
StreamHelper.copyStream(inputStream, out);
}
byte[] bytes = out.toByteArray();
System.out.println("response: " + bytes.length + " bytes");
result = new String(bytes);
} else {
result = response.readEntity(String.class);
}
} finally {
response.close();
}
System.out.println(result);
return result;
}
Form HTTP POST#
Альтернативным вариантом передачи Jexl-скрипта является отправка на адрес сервиса HTTP-формы, поле которой содержит Jexl-скрипт.
Адреса сервисов:
http://{server:port}/app/sys/rest/form/sessions/{id}/jexl/mainsel.http://{server:port}/app/sys/rest/form/sessions/new/jexl/mainsel?appname={имя_гл_выборки}.
В ответе вернётся команда перенаправления на страницу входа с идентификатором сессии в качестве HTTP-параметра.
Пример HTML-страницы
<html>
<body onload="onLoadFunc()">
<script>
function onLoadFunc() {
const form = document.createElement('form');
form.method = 'POST';
form.id = 'MultilinePostForm'
form.action = 'http://localhost:8080/app/sys/rest/form/sessions/new/jexl/mainsel?appname=Gs3_TestXmlApplication';
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = 'script';
hiddenField.value = `Btk_ClassAvi.list().newForm().open();
Gs3_RootTestAvi.list().newForm().open();`;
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
}
</script>
</body>
</html>