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#

Принцип работы сервиса#

  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-сессия существует в разрезе четырёх параметров:

  1. Алиас базы данных — получается из HTTP-заголовка Database. Если заголовок не передан, используется алиас из конфигурационного файла сервера.

  2. Имя пользователя — получается из авторизационных данных, передаётся в HTTP-заголовке Authorization.

  3. Имя прикладного пакета — получается из строки адреса.

  4. Рабочее пространство — получается из 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-сервис для взаимодействия с пользовательскими сессиями#

Сервис доступен только в режиме разработки. В проектных решениях недоступен.

Функциональность сервиса:

  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

Пример ответа:

{
  "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>