Перейти к основному содержанию
`Ctrl`+`K`

# 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>