1. Обзор дополнения

Универсальный REST API предоставляет следующую функциональность:

  • CRUD операции над сущностями.

  • Выполнение предопределенных JPQL запросов.

  • Вызов методов сервисов.

  • Получение метаданных (сущности, представления, перечисления, типы данных).

  • Получение разрешений для текущего пользователя (доступ к сущностям, атрибутам, специфические разрешения).

  • Получение информации о текущем пользователе (имя, язык, временная зона и т.д.).

  • Загрузка и скачивание файлов.

REST API использует протокол OAuth2 для аутентификации и поддерживает анонимный доступ.

Все методы REST API работают с данными согласно указанным в подсистеме безопасности разрешениям для авторизованного пользователя.

Tip

Подробная документация по REST API доступна по следующему адресу http://files.cuba-platform.com/swagger/7.2.

2. Примечания к выпускам

Release 7.2.6

Resolved issues:

  • Add-on should depend on Spring Security 5.5 #146

Release 7.2.5

Resolved issues:

  • CORS request fails in case of allowedOrigins set to all #145

  • NullPointerException if /search request finds nothing #144

Release 7.2.4

Resolved issues:

  • Character datatype isn’t supported on filter operation #134

  • Support service method result with generics #138

  • Clear refreshTokenValueToUserLoginStore map while removing refresh token from TokenStore #139

  • Fix security alerts #140

Release 7.2.3

Resolved issues:

  • Fix sorting by association attribute if the have null value #121

  • REST API does not support entities with composite primary key #49

  • Support for Java 8 Date/Time API in parameters #125

Release 7.2.2

Resolved issues:

Release 7.2.1

  • Добавлена предустановленная роль rest-api-access, включающая разрешение cuba.restApi.enabled.

Release 7.2.0

  • Поддержка платфомы версии 7.2.x.

Release 7.1.1

Resolved issue:

3. Начало работы

3.1. Установка

Для добавления аддона в проект выполните следующие действия:

  1. Кликните дважды Add-ons в дереве проекта CUBA.

    addons
  2. Перейдите на вкладку Marketplace и найдите аддон REST API.

    restapi addon
  3. Кликните Install и затем Apply & Close.

    addon install
  4. Кликните Continue в появившемся диалоговом окне.

    addon continue

Аддон, соответствующий используемой версии платформы, будет добавлен в проект.

Имейте в виду, что аддон предоставляется для платформы версии 7.1 и выше. В более ранних версиях REST API был включен в основной фреймворк.

3.2. Тестирование базовой функциональности

  • После окончания обновления Gradle в CUBA Studio в дереве проекта вы увидите элемент REST.

  • Настройте безопасность в соответствии с разделом Безопасность приложений:

    • Создайте роль, включающую специфическое разрешение cuba.restApi.enabled и дающую права на чтение нужных сущностей и атрибутов.

    • Назначьте созданную роль пользователю.

  • Запустите приложение и протестируйте REST API с помощью инструмента командной строки curl:

    1. Запрос OAuth токена:

      curl -X POST \
        http://localhost:8080/app/rest/v2/oauth/token \
        -H 'Authorization: Basic Y2xpZW50OnNlY3JldA==' \
        -H 'Content-Type: application/x-www-form-urlencoded' \
        -d 'grant_type=password&username=admin&password=admin'

      Вы получите результат в формате JSON с некоторым значением access_token. Используйте его для дальнейших запросов в заголовке Authorization.

    2. Запрос списка ролей (замените <access_token> значением, полученным на предыдущем шаге):

        curl -X GET \
        'http://localhost:8080/app/rest/v2/entities/sec$Role' \
        -H 'Authorization: Bearer <access_token>'

      Ответ будет содержать все зарегистрированные роли при условии, что пользователь, для которого был получен токен, имеет разрешение на чтение сущности sec$Role.

4. Возможности

Раздел содержит описание особенностей и конфигурационных параметров REST API. Раздел Использование REST API содержит примеры, демонстрирующие возможности REST API в действии.

4.1. Настройка предопределенных JPQL запросов

В приложении на CUBA предопределенные JPQL запросы должны быть объявлены в файлах, определенных свойством приложения cuba.rest.queriesConfig. Свойство должно быть определено в модуле web или portal (например, в файле web-app.properties):

cuba.rest.queriesConfig = +com/company/myapp/rest-queries.xml

Файл rest-queries.xml должен находиться в главном пакете модуля web или portal (например, com.company.myapp). Его содержимое определяется схемой rest-queries.xsd, например:

<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
    <query name="carByVin" entity="sample_Car" view="carEdit">
        <jpql><![CDATA[select c from sample_Car c where c.vin = :vin]]></jpql>
        <params>
            <param name="vin" type="java.lang.String"/>
        </params>
    </query>
    <query name="allColours" entity="sample_Colour" view="_local">
        <jpql><![CDATA[select u from sample_Colour u order by u.name]]></jpql>
    </query>
    <query name="carsByIds" entity="sample_Car" view="carEdit" cacheable="true">
        <jpql><![CDATA[select c from sample_Car c where c.id in :ids]]></jpql>
        <params>
            <param name="ids" type="java.util.UUID[]"/>
        </params>
    </query>
    <query name="myOrders" entity="sample_Order" view="orderBrowse">
        <jpql><![CDATA[select o from sample_Order o where o.createdBy = :session$userLogin]]></jpql>
    </query>
</queries>

Пример конфигурирования и исполнения запроса можно увидеть в разделе Выполнение JPQL-запроса (GET) и Выполнение JPQL-запроса (POST).

Платформа также предоставляет встроенный запрос all для получения списка всех экземпляров некоторой сущности. Он может быть использован совместно с /count, чтобы получить общее количество экземпляров сущности, например:

http://localhost:8080/app/rest/v2/queries/sales_Order/all/count

Атрибут cacheable элемента query включает кэширование данного запроса.

Запрос может содержать предопределенные параметры, которые принимают значения идентификатора и логина текущего пользователя: session$userId и session$userLogin. Их не нужно объявлять в элементе params (см. пример выше).

4.2. Настройка сервисов среднего слоя

Список методов сервисов, доступных для вызова через REST API, должен быть объявлен в приложении в конфигурационных файлах, заданных свойством приложения cuba.rest.servicesConfig. Свойство должно быть определено в модуле web или portal (например, в файле web-app.properties):

cuba.rest.servicesConfig = +com/company/myapp/rest-services.xml

Файл rest-services.xml должен находиться в главном пакете модуля web или portal (например, com.company.myapp). Его содержимое определяется схемой rest-services-v2.xsd, например:

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="myapp_SomeService">
        <method name="sum">
            <param name="number1"/>
            <param name="number2"/>
        </method>
        <method name="emptyMethod"/>
        <method name="overloadedMethod">
            <param name="intParam" type="int"/>
        </method>
        <method name="overloadedMethod">
            <param name="stringParam" type="java.lang.String"/>
        </method>
    </service>
</services>

Типы параметров метода могут быть опущены, если сервис не содержит перегруженного метода с тем же количеством аргументов. Иначе типы параметров обязательны к указанию.

Если тип параметра является примитивным типом, то достаточно просто указать имя типа (int, double, etc.) в файле конфигурации:

<param name="intParam" type="int"/>

В случае объектов, в том числе и сущностей, указывается полное имя класса:

<param name="stringParam" type="java.lang.String"/>
<param name="entityParam" type="com.company.entity.Order"/>

В случае коллекции объектов указывается тип коллекции:

<param name="entitiesCollectionParam" type="java.util.List"/>

Пример конфигурирования и вызова сервиса можно увидеть в разделе Вызов метода сервиса (GET).

Если необходимо иметь возможность вызова метода сервиса без аутентификации даже при отключенном анонимном доступе к REST API, то можно пометить метод сервиса атрибутом anonymousAllowed="true" в конфигурационном файле:

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="myapp_SomeService">
        <method name="sum" anonymousAllowed="true">
            <param name="number1"/>
            <param name="number2"/>
        </method>
    </service>
</services>

4.3. Версионирование модели данных

REST API поддерживает работу с различными версиями модели данных. Это может быть полезно в случае, например, когда имя атрибута сущности было изменено, но клиент REST API не знает об этом изменении и ожидает, что атрибут все еще имеет старое имя.

Для подобных случаев вы можете описать правила трансформации JSON сущностей. Если клиентское приложение посылает версию модели данных в параметре запроса, то JSON ответа, сформированном REST API, или тело запроса к REST API будут трансформированы согласно правилам, объявленным для конкретной версии модели данных.

Правила трансформации JSON должны быть объявлены в файлах, зарегистрированных в свойстве приложения cuba.rest.jsonTransformationConfig для модуля web или portal (например, в файле web-app.properties):

cuba.rest.jsonTransformationConfig = +com/company/myapp/rest-json-transformations.xml

Файл rest-json-transformations.xml должен быть расположен в модуле web или portal (например, в пакете com.company.myapp). Содержимое файла определяется схемой rest-json-transformations.xsd. Пример файла:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">

    <transformation modelVersion="1.0" oldEntityName="sales_OldOrder" currentEntityName="sales_NewOrder">
        <renameAttribute oldName="oldNumber" currentName="number"/>
        <renameAttribute oldName="date" currentName="deliveryDate"/>
        <toVersion>
            <removeAttribute name="discount"/>
        </toVersion>
    </transformation>

    <transformation modelVersion="1.0" currentEntityName="sales_Contractor">
        <renameAttribute oldName="summary" currentName="total"/>
        <renameAttribute oldName="familyName" currentName="lastName"/>
        <fromVersion>
            <removeAttribute name="city"/>
            <removeAttribute name="country"/>
        </fromVersion>
        <toVersion>
            <removeAttribute name="phone"/>
        </toVersion>
    </transformation>

    <transformation modelVersion="1.1" currentEntityName="sales_NewOrder">
        <renameAttribute oldName="date" currentName="deliveryDate"/>
    </transformation>

</transformations>

Стандартные трансформеры, определяемые в конфигурационном файле, могут осуществлять следующие типы трансформаций JSON:

  • переименование сущности

  • переименование атрибута сущности

  • удаление атрибута сущности

Трансформация JSON работает для следующих адресов REST API:

  • /entities - получение списка сущностей, одной сущности, создание сущности, изменение сущности, удаление сущности

  • /queries - JSON с сущностями, возвращаемыми методом, будет трансформирован

  • /services - трансформации JSON применяются как к сущностям, возвращаемым методом сервиса, так и к сущностям, переданным в качестве параметра метода.

Трансформации JSON применяются, если запрос к REST API содержит параметр modelVersion со значением версии модели данных в URL.

Раздел Примеры версионирования модели данных содержит примеры настройки версионирования модели данных и использования его из клиентских приложений.

4.4. Настройки CORS

По умолчанию все кросс-доменные запросы к REST API разрешены. Для ограничения списка разрешенных хостов укажите список хостов через запятую в свойстве приложения cuba.rest.allowedOrigins.

4.5. Анонимный доступ

По умолчанию анонимный доступ к REST API запрещен. Для его включения установите свойство приложения cuba.rest.anonymousEnabled в true. Запрос считается анонимным, если в нем отсутствует заголовок Authentication. В этом случае SecurityContext будет содержать анонимную сессию.

Чтобы определить разрешения для анонимного доступа, необходимо задать набор ролей для пользователя, имя которого хранится в свойстве приложения cuba.anonymousLogin.

4.6. Прочие настройки REST API

cuba.rest.client.id - определяет id клиента REST API по умолчанию.

cuba.rest.client.secret - определяет пароль клиента REST API по умолчанию.

cuba.rest.client.tokenExpirationTimeSec - определяет время жизни access токена в секундах для клиента по умолчанию.

cuba.rest.client.refreshTokenExpirationTimeSec - определяет время жизни refresh токена в секундах для клиента по умолчанию.

cuba.rest.client.authorizedGrantTypes - определяет список типов авторизации (grant type), поддерживаемых клиентом по умолчанию. Для отключения поддержки refresh-токенов, удалите элемент refresh_token из значения свойства.

cuba.rest.maxUploadSize - определяет максимальный размер файла, который может быть загружен с помощью REST API.

cuba.rest.reuseRefreshToken - определяет должен ли refresh-токен быть повторно использован.

cuba.rest.requiresSecurityToken - указывает, что в JSON сущности должен пересылаться дополнительный системный атрибут. Подробнее см. Ограничения для атрибутов-коллекций.

cuba.rest.tokenMaskingEnabled - указывает, должны ли значения токенов REST API быть маскированы в логах приложения.

4.7. Собственные контроллеры, защищенные OAuth2

Если вам необходимо создать свой REST контроллер, защищенный с помощью OAuth2, сделайте следующее:

  1. Предположим, ваш контроллер выглядит следующим образом:

    package com.company.test.portal.myapi;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import com.company.test.services.SomeService;
    
    @RestController
    @RequestMapping("/myapi")
    public class MyController {
    
        @Inject
        protected SomeService someService;
    
        @GetMapping("/dosmth")
        public String doSmth() {
            return someService.getResult();
        }
    }
  2. Создайте новый файл конфигурации Spring с именем rest-dispatcher-spring.xml внутри корневого пакета (например, com.company.test) модуля web или portal. Содержимое файла должно быть следующим:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:security="http://www.springframework.org/schema/security">
    
        <!-- Define a base package for your controllers-->
        <context:component-scan base-package="com.company.test.portal.myapi"/>
    
        <security:http pattern="/rest/myapi/**"
                       create-session="stateless"
                       entry-point-ref="oauthAuthenticationEntryPoint"
                       xmlns="http://www.springframework.org/schema/security">
            <!-- Specify one or more protected URL patterns-->
            <intercept-url pattern="/rest/myapi/**" access="isAuthenticated()"/>
            <anonymous enabled="false"/>
            <csrf disabled="true"/>
            <cors configuration-source-ref="cuba_RestCorsSource"/>
            <custom-filter ref="resourceFilter" before="PRE_AUTH_FILTER"/>
            <custom-filter ref="cuba_AnonymousAuthenticationFilter" after="PRE_AUTH_FILTER"/>
        </security:http>
    </beans>
  3. Задайте аддитивное свойство приложения cuba.restSpringContextConfig в файле свойств соответствующего модуля, например, в portal-app.properties:

    cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
  4. Новый контроллер будет помещен в контекст, связанный с сервлетом CubaRestApiServlet, поэтому URL для доступа к методам контроллера будут начинаться с /rest, т.е. метод doSmth() будет доступен по адресу: http://localhost:8080/app-portal/rest/myapi/dosmth.

    Warning

    Адреса для доступа к методам кастомных контроллеров НЕ ДОЛЖНЫ начинаться с /rest/v2.

4.8. Ограничения для атрибутов-коллекций

Рассмотрим следующую ситуацию:

  • Модель данных содержит сущности Order и OrderLine, образующие one-to-many композицию.

  • Некоторый REST-клиент загружает экземпляр Order вместе с вложенной коллекцией экземпляров OrderLine.

  • Существуют ограничения безопасности, которые отфильтровывают некоторые экземпляры OrderLine так, что клиент не загружает их и не знает об их существовании. Допустим, строка line5 не загружена клиентом, но существует в базе данных.

  • Если клиент удаляет из коллекции некоторую строку, скажем, line2, и сохраняет всю композицию с помощью запроса на /entities/{entityName}/{entityId}, то возможны два исхода:

    1. Если ограничения не были изменены с момента загрузки объектов, фреймворк восстанавливает отфильтрованный элемент коллекции line5 и удаляет только line2, что является корректным поведением.

    2. Если ограничения были изменены таким образом, что line5 стал доступным клиенту, фреймворк не может корректно восстановить информацию об отфильтрованных элементах коллекции. В результате будут удалены оба элемента: line2 и line5.

Если в вашем приложении подобная ситуация возможна, то избежать потери данных можно путем пересылки в JSON специального системного атрибута. Данный атрибут имеет имя __securityToken и автоматически включается в результирующий JSON если свойство приложения cuba.rest.requiresSecurityToken установлено в true. В этом случае в обязанности вашего REST-клиента входит передача этого атрибута обратно при сохранении сущности.

Пример JSON сущности с включенным security token:

{
  "id": "fa430b56-ceb2-150f-6a85-12c691908bd1",
  "number": "OR-000001",
  "lines": [
    {
      "id": "82e6e6d2-be97-c81c-c58d-5e2760ae095a",
      "description": "Item 1"
    },
    {
      "id": "988a8cb5-d61a-e493-c401-f717dd9a2d66",
      "description": "Item 2"
    }
  ],
  "__securityToken": "0NXc6bQh+vZuXE4Fsk4mJX4QnhS3lOBfxzUniltchpxPfi1rZ5htEmekfV60sbEuWUykbDoY+rCxdhzORaYQNQ=="
}

Атрибут __securityToken содержит закодированные идентификаторы отфильтрованных экземпляров, так что фреймворк всегда может восстановить необходимую информацию независимо от изменений в настройках ограничений.

4.9. Персистентное хранилище токенов

По умолчанию OAuth токены хранятся только в памяти. Для того, чтобы параллельно хранить их в базе данных, установите свойство cuba.rest.storeTokensInDb в true. Значение свойства хранится в базе данных, следовательно, редактировать его можно из экрана Администрирование > Свойства приложения.

Истекшие токены должны периодически удаляться из базы данных. Выражение cron, определяющее расписание процедуры удаления, определено свойством приложения cuba.rest.deleteExpiredTokensCron.

4.10. Swagger документация по проекту

Документация по обобщенному REST API доступна по адресу http://files.cuba-platform.com/swagger/7.2.

Любое запущенное приложение на CUBA также экспортирует документацию конкретно для данного проекта, сгенерированную в соответствии со спецификацией Swagger версии 2.0.

Генерируемая документация доступна по следующим адресам:

  • /rest/v2/docs/swagger.yaml - YAML-версия общей документации.

  • /rest/v2/docs/swagger.json - JSON-версия общей документации.

  • /rest/v2/docs/swaggerDetailed.yaml - YAML-версия проектной документации.

  • /rest/v2/docs/swaggerDetailed.json - JSON-версия проектной документации.

Пример:

http://localhost:8080/app/rest/v2/docs/swagger.yaml
http://localhost:8080/app/rest/v2/docs/swaggerDetailed.yaml

Проектная документация может использоваться для визуализации, тестирования, или генерации клиентского кода для REST API. См. следующие инструменты: Swagger UI, Swagger Inspector, Postman, Swagger Codegen.

Документация включает в себя:

  1. CRUD-операции, такие, как:

    Также для всех параметров и ответов CRUD доступна подробная модель, к примеру:

    swagger crud model
  2. Предопределённые запросы JPQL:

    swagger query
  3. Сервисы, доступные через REST:

    swagger service

5. Использование REST API

Данный раздел содержит ряд примеров использования REST API.

Tip

Детальная информация о методах REST API доступна по адресу http://files.cuba-platform.com/swagger/7.2.

Убедитесь, что роль с включенным REST API назначена пользователю.

5.1. Получение OAuth токена

OAuth токен необходим для выполнения любого метода REST API (кроме случая анонимного доступа к REST API). Получить токен можно выполнив POST запрос по адресу

http://localhost:8080/app/rest/v2/oauth/token

Доступ к данному URL защищен с помощью базовой аутентификации с использованием идентификатора и пароля клиента REST API. Обратите внимание, что это не логин и пароль пользователя приложения. Идентификатор и пароль клиента REST API генерируются при добавлении аддона. Значения можно посмотреть или изменить в свойствах cuba.rest.client.id и cuba.rest.client.secret файла web-app.properties. Заголовок Authorization запроса на получение токена должен содержать логин и пароль клиента, разделенные символом ":" и закодированные в base64.

Тип запроса на получение токена должен быть application/x-www-form-urlencoded, кодировка UTF-8.

Запрос должен содержать следующие параметры:

  • grant_type - всегда значение password.

  • username - логин пользователя приложения.

  • password - пароль пользователя приложения.

Например, для значений свойств

cuba.rest.client.id=client
cuba.rest.client.secret={noop}secret

запрос на получение токена выглядит следующим образом:

POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=smith&password=qwerty123

Вы также можете использовать утилиту cURL:

curl -H "Content-type: application/x-www-form-urlencoded" -H "Authorization: Basic Y2xpZW50OnNlY3JldA==" -d "grant_type=password&username=admin&password=admin" http://localhost:8080/app/rest/v2/oauth/token

Метод возвращает JSON объект:

{
  "access_token": "29bc6b45-83cd-4050-8c7a-2a8a60adf251",
  "token_type": "bearer",
  "refresh_token": "e765446f-d49e-4634-a6d3-2d0583a0e7ea",
  "expires_in": 43198,
  "scope": "rest-api"
}

Значение токена содержится в поле access_token

Чтобы использовать токен, его нужно передать в заголовке запроса Authorization с типом Bearer, например:

Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251

Свойство refresh_token содержит значение refresh токена. Он не может быть использован для доступа к защищённым ресурсам приложения, но он имеет большее время жизни по сравнению с access токеном и используется для получения нового access токена, когда старый токен истек.

Запрос на получение нового access токена с помощью refresh токена должен содержать следующие параметры:

  • grant_type - refresh_token.

  • refresh_token - значения refresh токена.

POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=e765446f-d49e-4634-a6d3-2d0583a0e7ea

См. также следующие свойства приложения, имеющие отношение к токенам:

5.2. Аутентификация LDAP в REST API

Для настройки LDAP аутентификации в REST API используются следующие свойства приложения:

  • cuba.rest.ldap.enabled - определяет включен ли вход по LDAP логину/паролю.

  • cuba.rest.ldap.urls - URL сервера LDAP.

  • cuba.rest.ldap.base - base DN поиска имен пользователей.

  • cuba.rest.ldap.user - distinguished name системного пользователя, имеющего право на чтение информации из LDAP.

  • cuba.rest.ldap.password - пароль системного пользователя, заданного свойством cuba.web.ldap.user.

  • cuba.rest.ldap.userLoginField - название атрибута пользователя в LDAP, значение которого соответствует логину пользователя. По умолчанию sAMAccountName (подходит для Active Directory).

Пример содержимого файла local.app.properties:

cuba.rest.ldap.enabled = true
cuba.rest.ldap.urls = ldap://192.168.1.1:389
cuba.rest.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.password = system_user_password

Получить OAuth токен можно выполнив POST запрос по адресу:

http://localhost:8080/app/rest/v2/ldap/token

Доступ к данному URL защищен с помощью базовой аутентификации с использованием идентификатора и пароля клиента REST API. Обратите внимание, что это не логин и пароль пользователя приложения. Идентификатор и пароль клиента REST API генерируются при добавлении аддона. Значения можно посмотреть или изменить в свойствах cuba.rest.client.id и cuba.rest.client.secret файла web-app.properties. Заголовок Authorization запроса на получение токена должен содержать логин и пароль клиента, разделенные символом ":" и закодированные в base64.

Запрос должен содержать следующие параметры (соответствуют параметрам стандартной аутентификации):

  • grant_type - всегда значение password.

  • username - логин пользователя приложения.

  • password - пароль пользователя приложения.

Тип запроса на получение токена должен быть application/x-www-form-urlencoded, кодировка UTF-8.

Стандартная аутентификация может быть отключена при помощи свойства cuba.rest.standardAuthenticationEnabled:

cuba.rest.standardAuthenticationEnabled = false

5.3. Собственный механизм аутентификации

Различные механизмы аутентификации могут предоставлять токен по ключу, по ссылке, по логину и паролю LDAP и т.д. Стандартный механизм аутентификации в REST API изменить нельзя, но можно создать свой механизм. Для этого необходимо создать REST-контроллер, который предоставит свой URL для входа в приложение.

В этом примере мы рассмотрим механизм аутентификации, позволяющий получить OAuth-токен по промо-коду. За основу возьмём приложение, содержащее сущность Coupon (Купон) с атрибутом code (промо-код). Значение этого атрибута мы будем передавать в качестве параметра аутентификации в GET-запросе.

  1. Создайте сущность Coupon и добавьте ей атрибут code:

    @Column(name = "CODE", unique = true, length = 4)
    protected String code;
  2. Создайте роль, включающую специфическое разрешение cuba.restApi.enabled и дающую права на чтение нужных сущностей и атрибутов.

  3. Создайте нового пользователя с логином promo-user, от лица которого будет выполняться аутентификация по промо-коду.

  4. Назначьте созданную роль пользователю promo-user.

  5. В корневом каталоге модуля web (com.company.demo) создайте новый файл конфигурации Spring rest-dispatcher-spring.xml со следующим содержанием:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
                               http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
                               http://www.springframework.org/schema/context
                               http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
        <context:component-scan base-package="com.company.demo.web.rest"/>
    
    </beans>
  6. Ссылку на этот файл укажите в свойстве приложения cuba.restSpringContextConfig в файле modules/web/src/web-app.properties:

    cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
  7. Создайте пакет rest в корневом каталоге модуля web, а в нём - свой контроллер Spring MVC. В контроллере используйте бин OAuthTokenIssuer, который позволяет сгенерировать и выдать REST API токен после аутентификации:

    @RestController
    @RequestMapping("auth-code")
    public class AuthCodeController {
    
        @Inject
        private OAuthTokenIssuer oAuthTokenIssuer;
        @Inject
        private Configuration configuration;
        @Inject
        private DataManager dataManager;
        @Inject
        private MessageTools messageTools;
        @Inject
        private TrustedClientService trustedClientService;
    
        // here we check secret code and issue token using OAuthTokenIssuer
        @RequestMapping(method = RequestMethod.GET)
        public ResponseEntity get(@RequestParam("code") String authCode) {
            // obtain system session to be able to call middleware services
            WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);
            UserSession systemSession;
            try {
                systemSession = trustedClientService.getSystemSession(webAuthConfig.getTrustedClientPassword());
            } catch (LoginException e) {
                throw new RuntimeException("Error during system auth");
            }
    
            // set security context
            AppContext.setSecurityContext(new SecurityContext(systemSession));
            try {
                // find coupon with code
                LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class)
                        .setQuery(LoadContext.createQuery("select c from demo_Coupon c where c.code = :code")
                                .setParameter("code", authCode));
    
                if (dataManager.load(loadContext) == null) {
                    // if coupon is not found - code is incorrect
                    return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST);
                }
    
                // generate token for "promo-user"
                OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult =
                        oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap());
                OAuth2AccessToken accessToken = tokenResult.getAccessToken();
    
                // set security HTTP headers to prevent browser caching of security token
                HttpHeaders headers = new HttpHeaders();
                headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
                headers.set(HttpHeaders.PRAGMA, "no-cache");
                return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
            } finally {
                // clean up security context
                AppContext.setSecurityContext(null);
            }
        }
    
        // POJO for JSON error messages
        public static class ErrorInfo implements Serializable {
            private String error;
            private String error_description;
    
            public ErrorInfo(String error, String error_description) {
                this.error = error;
                this.error_description = error_description;
            }
    
            public String getError() {
                return error;
            }
    
            public String getError_description() {
                return error_description;
            }
        }
    }
  8. Исключите пакет rest из сканирования в модулях web/core: это необходимо, так как бин OAuthTokenIssuer доступен только внутри контекста REST API, и сканирование его в контексте приложения будет вызывать ошибку.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/>
    </context:component-scan>
  9. Теперь пользователи могут получать код доступа OAuth2 через обычный запрос GET HTTP, передавая значение промо-кода в параметре code:

    http://localhost:8080/app/rest/auth-code?code=A325

    Результат:

    {"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}

    Теперь полученный access token нужно передавать в REST API, как описано в общей документации.

5.3.1. Social Login в REST API

Механизм авторизации через социальные сети, или social login, также можно использовать в REST API. Исходный код приложения, описанного в этом примере, доступен на GitHub, а сам пример описан в разделе social login. Ниже приведены ключевые моменты этого примера, позволяющие получить OAuth-токен через аккаунт Facebook.

  1. Создайте пакет restapi в корневом каталоге модуля web и поместите в него собственный контроллер Spring MVC. Контроллер должен содержать два основных метода: get(), возвращающий ResponseEntity, и login(), в котором мы будем получать OAuth-токен.

    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity get() {
        String loginUrl = getAsPrivilegedUser(() ->
                facebookService.getLoginUrl(getAppUrl(), OAuth2ResponseType.CODE_TOKEN)
        );
    
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.LOCATION, loginUrl);
        return new ResponseEntity<>(headers, HttpStatus.FOUND);
    }

    Здесь мы проверяем переданный код Facebook, получаем код доступа и издаём токен с помощью OAuthTokenIssuer:

    @RequestMapping(method = RequestMethod.POST, value = "login")
    public ResponseEntity<OAuth2AccessToken> login(@RequestParam("code") String code) {
        User user = getAsPrivilegedUser(() -> {
            FacebookUserData userData = facebookService.getUserData(getAppUrl(), code);
    
            return socialRegistrationService.findOrRegisterUser(
                userData.getId(), userData.getEmail(), userData.getName());
        });
    
        OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken(user.getLogin(),
                messageTools.getDefaultLocale(), Collections.emptyMap());
    
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
        headers.set(HttpHeaders.PRAGMA, "no-cache");
        return new ResponseEntity<>(tokenResult.getAccessToken(), headers, HttpStatus.OK);
    }
  2. Исключите пакет restapi из сканирования в модулях web/core: это необходимо, так как бин OAuthTokenIssuer доступен только внутри контекста REST API, и сканирование его в контексте приложения будет вызывать ошибку.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.restapi\..*"/>
    </context:component-scan>
  3. В корневом каталоге модуля web (com.company.demo) создайте новый файл конфигурации Spring rest-dispatcher-spring.xml со следующим содержанием:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="
               http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
               http://www.springframework.org/schema/context
               http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
        <context:component-scan base-package="com.company.demo.restapi"/>
    
    </beans>
  4. Ссылку на этот файл укажите в свойстве приложения cuba.restSpringContextConfig в файле modules/web/src/web-app.properties:

    cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
  5. Загрузите jQuery и скопируйте в каталог modules/web/web/VAADIN вашего проекта.

  6. Создайте файл facebook-login-demo.html в каталоге проекта modules/web/web/VAADIN. Он будет содержать JavaScript-код, выполняющийся на HTML-странице:

    <html>
    <head>
        <title>Facebook login demo with REST-API</title>
        <script src="jquery-3.5.1.min.js"></script>
        <style type="text/css">
            #users {
                display: none;
            }
        </style>
    </head>
    <body>
    <h1>Facebook login demo with REST-API</h1>
    
    <script type="application/javascript"...>
    </script>
    
    <a id="fbLink" href="/app/rest/facebook">Login with Facebook</a>
    
    <div id="users">
        You are logged in!
    
        <h1>Users</h1>
    
        <div id="usersList">
        </div>
    </div>
    
    </body>
    </html>

    В этом скрипте мы попробуем залогиниться через Facebook. Сначала удаляем лишний код из URL, затем передаём код в REST API для получения OAuth-токена, и в случае успешной аутентификации мы сможем загружать и сохранять данные как обычно:

    var oauth2Token = null;
    
    function tryToLoginWithFacebook() {
        var urlHash = window.location.hash;
    
        if (urlHash && urlHash.indexOf('&code=') >= 0) {
            console.log("Try to login to CUBA REST-API!");
    
            var urlCode = urlHash.substring(urlHash.indexOf('&code=') + '&code='.length);
            console.log("Facebook code: " + urlCode);
    
            history.pushState("", document.title, window.location.pathname);
    
            $.post({
                url: '/app/rest/facebook/login',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                dataType: 'json',
                data: {code: urlCode},
                success: function (data) {
                    oauth2Token = data.access_token;
    
                    loadUsers();
                }
            })
        }
    }
    
    function loadUsers() {
        $.get({
            url: '/app/rest/v2/entities/sec$User?view=_local',
            headers: {
                'Authorization': 'Bearer ' + oauth2Token,
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            success: function (data) {
                $('#fbLink').hide();
                $('#users').show();
    
                $.each(data, function (i, user) {
                    $('#usersList').append("<li>" + user.name + " (" + user.email + ")</li>");
                });
            }
        });
    }
    
    tryToLoginWithFacebook();

    Другой пример использования JavaScript-кода в приложениях CUBA вы можете найти в разделе Пример использования из JavaScript.

5.4. Получение списка экземпляров сущности

Предположим, в системе имеется сущность sales_Order, и необходимо получить список экземпляров этой сущности. При этом, необходимо получить не все записи, а 50 записей, начиная с сотой (для отображения третьей странице в каком-либо списке клиентского приложения). Кроме простых атрибутов сущности sales_Order результат должен содержать данные о клиенте (поле customer). Заказы должны быть отсортированы по дате.

Базовый URL для получения списка экземпляров сущности sales_Order:

http://localhost:8080/app/rest/v2/entities/sales_Order

Для выполнения описанных выше условий необходимо задать параметры запроса:

  • view - представление, с которым должны быть загружены сущности. В нашем примере представление order-edit содержит ссылку на customer.

  • limit - количество возвращаемых экземпляров.

  • offset - позиция первого извлеченного элемента.

  • sort - имя атрибута сущности, по которому будет произведена сортировка.

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer:

Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251

В итоге получаем следующий GET http-запрос:

http://localhost:8080/app/rest/v2/entities/sales_Order?view=order-edit&limit=50&offset=100&sort=date

В cURL запрос может выглядеть так:

curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" http://localhost:8080/app/rest/v2/entities/sales_Order?view=order-edit&limit=50&offset=100&sort=date

Ответ будет выглядеть следующим образом:

[
  {
      "_entityName": "sales_Order",
      "_instanceName": "13/12/2017 Sidney Chandler",
      "id": "87fe4332-87fe-4332-870c-5875870c5875",
      "date": "2017-12-13",
      "amount": 49.99,
      "lines": [
          {
              "_entityName": "sales_OrderLine",
              "_instanceName": "Outback Power Remote Power System",
              "id": "9a8064af-6bdd-58e9-6672-f4a1a7a79480",
              "product": {
                  "_entityName": "sales_Product",
                  "_instanceName": "Outback Power Remote Power System",
                  "id": "e8f27ecf-4024-aa12-9362-7a11f150e65a",
                  "price": 49.99,
                  "name": "Outback Power Remote Power System"
              },
              "quantity": 1.000
          }
      ],
      "customer": {
          "_entityName": "sales_Customer",
          "_instanceName": "Sidney Chandler",
          "id": "a09983f6-540c-5d49-a4cf-d2343b6ee44b",
          "name": "Sidney Chandler"
      }
  },
  {
      "_entityName": "sales_Order",
      "_instanceName": "22/12/2017 Shelby Robinson",
      "id": "f12a4193-f12a-4193-c83d-2a46c83d2a46",
      "date": "2017-12-22",
      "amount": 283.55,
      "lines": [
          {
              "_entityName": "sales_OrderLine",
              "_instanceName": "Cotek Battery Charger",
              "id": "4ec120de-1ced-ad44-5817-fec5e632e3cd",
              "product": {
                  "_entityName": "sales_Product",
                  "_instanceName": "Cotek Battery Charger",
                  "id": "008f9335-c3a4-8e3b-cb59-a2e6b1e6b283",
                  "price": 30.10,
                  "name": "Cotek Battery Charger"
              },
              "quantity": 1.000
          },
          {
              "_entityName": "sales_OrderLine",
              "_instanceName": "Solar-One HUP Flooded Battery 48V",
              "id": "4179ea59-a927-5754-c7b2-17a02f9f4df7",
              "product": {
                  "_entityName": "sales_Product",
                  "_instanceName": "Solar-One HUP Flooded Battery 48V",
                  "id": "1d2272b5-a702-e3d4-6d2c-7cd360f93182",
                  "price": 210.55,
                  "name": "Solar-One HUP Flooded Battery 48V"
              },
              "quantity": 1.000
          }
      ],
      "customer": {
          "_entityName": "sales_Customer",
          "_instanceName": "Shelby Robinson",
          "id": "7efdaa07-0844-749c-6b43-a1b3a8b2b803",
          "name": "Shelby Robinson"
      }
  }
]

Обратите внимание, что для каждой сущности загружаются атрибуты _entityName с именем сущности и _instanceName, содержащий результат вычисления короткого имени для сущности.

5.5. Создание экземпляра сущности

Для создания нового экземпляра сущности sales_Order необходимо выполнить POST запрос по адресу:

http://localhost:8080/app/rest/v2/entities/sales_Order

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

Тело запроса должно содержать JSON объект, описывающий новый экземпляр, например:

{
  "date": "2020-01-12",
  "lines": [
    {
      "_entityName": "sales_OrderLine",
      "product": {
        "_entityName": "sales_Product",
        "id": "e62906e5-1f1a-1bf1-9a57-c798293e86d7"
          },
      "quantity": 20
    },
    {
      "_entityName": "sales_OrderLine",
      "product": {
        "_entityName": "sales_Product",
        "id": "345e429f-6d74-7d1a-d1ab-9f6c67c10fe5"
          },
      "quantity": 5
    }
  ],
  "customer": {
    "id": "a09983f6-540c-5d49-a4cf-d2343b6ee44b"
  }
}

Ниже приведен пример POST запроса для создания нового экземпляра Order в cURL:

curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" -H "Content-Type: application/json" -X POST -d "{\"date\": \"2018-10-12 15:47:28\", \"amount\":  9.90, \"customer\": {\"id\": \"383ebce2-b295-7378-36a1-bcf93693821f\"}}" http://localhost:8080/app/rest/v2/entities/sales_Order

В теле запроса передается коллекция позиций заказа lines и ссылка на клиента customer. Рассмотрим, как будут обработаны эти атрибуты.

Сначала посмотрим на класс Order:

package com.company.sales.entity;

import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.cuba.core.entity.StandardEntity;
import com.haulmont.cuba.core.entity.annotation.Lookup;
import com.haulmont.cuba.core.entity.annotation.LookupType;
import com.haulmont.cuba.core.entity.annotation.OnDelete;
import com.haulmont.cuba.core.global.DeletePolicy;

import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

@NamePattern("%s %s|date,customer")
@Table(name = "SALES_ORDER")
@Entity(name = "sales_Order")
public class Order extends StandardEntity {
    private static final long serialVersionUID = 5602880376063487368L;

    @Temporal(TemporalType.DATE)
    @Column(name = "DATE_", nullable = false)
    protected Date date;

    @Column(name = "NUMBER_")
    private String number;

    @Composition
    @OnDelete(DeletePolicy.CASCADE)
    @OneToMany(mappedBy = "order")
    protected List<OrderLine> lines;

    @Column(name = "AMOUNT")
    protected BigDecimal amount;

    @Lookup(type = LookupType.DROPDOWN, actions = {"lookup", "open"})
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID")
    protected Customer customer;

    @Column(name = "NUMBER_OF_SPECIAL_PRODUCTS")
    protected Integer numberOfSpecialProducts;

    //getters and setters omitted
}

В сущности Order коллекция lines аннотирована @Composition. Методы создания и обновления сущности REST API создают новые экземпляры для всех элементов таких коллекций. Т.е. вместе с заказом (Order) будет создано две позиции заказа (OrderLine).

Ссылка customer не имеет аннотации @Composition, поэтому метод REST API попытается найти клиента с переданным идентификатором и проставить его в поле customer. Если клиент не будет найден, заказ не будет создан и метод вернет ошибку. Аналогично в поле product созданных позиций заказа будут проставлены указанные идентификаторы товаров.

В случае успеха возвращается JSON, содержащий только три поля:

{
  "_entityName": "sales_Order",
  "_instanceName": "12/01/2020 Sidney Chandler",
  "id": "b8e702a8-4ca7-47b6-5c76-3a35e33fe609"
}

Если в ответе необходимо отобразить дополнительные поля, тогда в запрос нужно добавить параметр responseView и указать одно из представлений. Например, для запроса с представлением order-edit

http://localhost:8080/app/rest/v2/entities/sales_Order?responseView=order-edit

ответ будет выглядеть следующим образом:

{
    "_entityName": "sales_Order",
    "_instanceName": "12/01/2020 Sidney Chandler",
    "id": "b8e702a8-4ca7-47b6-5c76-3a35e33fe609",
    "date": "2020-01-12",
    "numberOfSpecialProducts": 0,
    "lines": [
        {
            "_entityName": "sales_OrderLine",
            "_instanceName": "Astronergy Solar Panel",
            "id": "c3c7bbe4-75c5-0223-df56-0f6c018dd1e2",
            "product": {
                "_entityName": "sales_Product",
                "_instanceName": "Astronergy Solar Panel",
                "id": "345e429f-6d74-7d1a-d1ab-9f6c67c10fe5",
                "price": 23.00,
                "name": "Astronergy Solar Panel"
            },
            "quantity": 5.000
        },
        {
            "_entityName": "sales_OrderLine",
            "_instanceName": "Fullriver Sealed Battery 6V",
            "id": "881c6ea7-e9f4-cb2f-b0e3-1aac85aed81f",
            "product": {
                "_entityName": "sales_Product",
                "_instanceName": "Fullriver Sealed Battery 6V",
                "id": "e62906e5-1f1a-1bf1-9a57-c798293e86d7",
                "price": 5.10,
                "name": "Fullriver Sealed Battery 6V"
            },
            "quantity": 20.000
        }
    ],
    "customer": {
        "_entityName": "sales_Customer",
        "_instanceName": "Sidney Chandler",
        "id": "a09983f6-540c-5d49-a4cf-d2343b6ee44b",
        "name": "Sidney Chandler"
    }
}

Возможность указать параметр responseView задается с помощью свойства cuba.rest.responseViewEnabled, значение по умолчанию true. При отключенном свойстве в ответе возвращается полный граф созданной сущности.

Например, если cuba.rest.responseViewEnabled установлено в false, то ответ будет выглядеть следующим образом:

{
    "_entityName": "sales_Order",
    "id": "b8e702a8-4ca7-47b6-5c76-3a35e33fe609",
    "date": "2020-01-12",
    "updatedBy": "admin",
    "version": 2,
    "numberOfSpecialProducts": 0,
    "createdBy": "admin",
    "createTs": "2020-11-24 14:57:02.139",
    "lines": [
        {
            "_entityName": "sales_OrderLine",
            "id": "fcf41257-890f-7d57-5a5c-b3c4d75df40c",
            "product": {
                "_entityName": "sales_Product",
                "id": "345e429f-6d74-7d1a-d1ab-9f6c67c10fe5",
                "createdBy": "admin",
                "price": 23.00,
                "name": "Astronergy Solar Panel",
                "createTs": "2017-12-19 11:43:20.000",
                "version": 1,
                "updateTs": "2017-12-19 11:43:20.000"
            },
            "quantity": 5.000,
            "createdBy": "admin",
            "createTs": "2020-11-24 14:57:02.142",
            "version": 1,
            "updateTs": "2020-11-24 14:57:02.142",
            "order": {
                "_entityName": "sales_Order",
                "id": "9edf23b1-1698-7e08-4037-03d822876f08"
            }
        },
        {
            "_entityName": "sales_OrderLine",
            "id": "91cf2899-6c1d-4ac9-192f-24e1b08c7ea6",
            "product": {
                "_entityName": "sales_Product",
                "id": "e62906e5-1f1a-1bf1-9a57-c798293e86d7",
                "createdBy": "admin",
                "price": 5.10,
                "name": "Fullriver Sealed Battery 6V",
                "createTs": "2017-12-19 11:39:22.000",
                "version": 1,
                "updateTs": "2017-12-19 11:39:22.000"
            },
            "quantity": 20.000,
            "createdBy": "admin",
            "createTs": "2020-11-24 14:57:02.142",
            "version": 1,
            "updateTs": "2020-11-24 14:57:02.142",
            "order": {
                "_entityName": "sales_Order",
                "id": "9edf23b1-1698-7e08-4037-03d822876f08"
            }
        }
    ],
    "updateTs": "2020-11-24 14:57:02.252",
    "customer": {
        "_entityName": "sales_Customer",
        "id": "a09983f6-540c-5d49-a4cf-d2343b6ee44b",
        "createdBy": "admin",
        "name": "Sidney Chandler",
        "createTs": "2017-12-19 11:32:24.000",
        "version": 1,
        "updateTs": "2017-12-19 11:32:24.000",
        "email": "chandler@mail.com"
    }
}

5.6. Изменение существующего экземпляра сущности

Для изменения экземпляра сущности sales_Order необходимо выполнить PUT запрос по адресу:

http://localhost:8080/app/rest/v2/entities/sales_Order/050af491-16ad-7b4d-8499-4d0bace23bb1

Последняя часть запроса здесь - это идентификатор изменяемой сущности.

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

В теле запроса необходимо передать JSON объект, содержащий только поля, которые мы хотим изменить, например:

{
  "date": "2020-11-01",
  "customer": {
    "id": "5effc3b8-9445-2b86-cc30-92aabc7c3a6a"
  }
}

Тело ответа будет содержать три поля:

{
  "_entityName": "sales_Order",
  "_instanceName": "01/11/2020 Hildred Ellis",
  "id": "050af491-16ad-7b4d-8499-4d0bace23bb1"
}

Если в ответе необходимо отобразить дополнительные поля, тогда в запрос нужно добавить параметр responseView и указать одно из представлений. Например, для запроса с представлением order-edit

http://localhost:8080/app/rest/v2/entities/sales_Order/050af491-16ad-7b4d-8499-4d0bace23bb1?responseView=order-edit

ответ будет выглядеть следующим образом:

{
    "_entityName": "sales_Order",
    "_instanceName": "01/11/2020 Hildred Ellis",
    "id": "050af491-16ad-7b4d-8499-4d0bace23bb1",
    "date": "2020-11-01",
    "numberOfSpecialProducts": 0,
    "lines": [
        {
            "_entityName": "sales_OrderLine",
            "_instanceName": "Fullriver Sealed Battery 6V",
            "id": "8b6a921e-b910-c766-7e40-eed95fa49238",
            "product": {
                "_entityName": "sales_Product",
                "_instanceName": "Fullriver Sealed Battery 6V",
                "id": "e62906e5-1f1a-1bf1-9a57-c798293e86d7",
                "price": 5.10,
                "name": "Fullriver Sealed Battery 6V"
            },
            "quantity": 20.000
        }
    ],
    "customer": {
        "_entityName": "sales_Customer",
        "_instanceName": "Hildred Ellis",
        "id": "5effc3b8-9445-2b86-cc30-92aabc7c3a6a",
        "name": "Hildred Ellis"
    }
}

Возможность указать параметр responseView задается с помощью свойства cuba.rest.responseViewEnabled, значение по умолчанию true. При отключенном свойстве в ответе возвращается полный граф измененной сущности.

Например, если cuba.rest.responseViewEnabled установлено в false, то ответ будет выглядеть следующим образом:

{
    "_entityName": "sales_Order",
    "id": "050af491-16ad-7b4d-8499-4d0bace23bb1",
    "date": "2020-11-01",
    "updatedBy": "admin",
    "version": 6,
    "numberOfSpecialProducts": 0,
    "createdBy": "admin",
    "createTs": "2020-11-23 22:33:26.057",
    "updateTs": "2020-11-25 00:54:49.390",
    "customer": {
        "_entityName": "sales_Customer",
        "id": "5effc3b8-9445-2b86-cc30-92aabc7c3a6a",
        "createdBy": "admin",
        "name": "Hildred Ellis",
        "createTs": "2017-12-19 11:32:47.000",
        "version": 1,
        "updateTs": "2017-12-19 11:32:47.000",
        "email": "ellis@mail.com"
    }
}
Tip

JSON объект из примера выше содержит атрибут version, это означает, что сущность sales_Order реализует интерфейс Versioned и, таким образом, поддерживает оптимистичную блокировку. Чтобы включить оптимистичную блокировку сущностей, установите свойство приложения cuba.rest.optimisticLockingEnabled в true. Обратите внимание, что если атрибут version не содержится в JSON, оптимистичная блокировка работать не будет.

5.7. Выполнение JPQL-запроса (GET)

Перед выполнением запроса с помощью REST API необходимо описать его в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл rest-queries.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties):

cuba.rest.queriesConfig = +com/company/sales/rest-queries.xml

Содержимое файла rest-queries.xml:

<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
    <query name="ordersAfterDate" entity="sales_Order" view="order-edit">
        <jpql><![CDATA[select o from sales_Order o where o.date >= :startDate and o.date <= :endDate]]></jpql>
        <params>
            <param name="startDate" type="java.util.Date"/>
            <param name="endDate" type="java.util.Date"/>
        </params>
    </query>
</queries>

Для выполнения JPQL запроса, необходимо выполнить GET http-запрос к REST API:

http://localhost:8080/app/rest/v2/queries/sales_Order/ordersAfterDate?startDate=2016-11-01&endDate=2017-11-01

Части URL:

  • sales_Order - имя извлекаемой сущности.

  • ordersAfterDate - имя запроса из конфигурационного файла.

  • startDate и endDate - параметры запроса со значениями.

При передаче значения параметра запроса необходимо использовать формат из соответствующего datatype. Например:

  • если тип параметра java.util.Date, то формат значения берётся из DateTimeDatatype. По умолчанию это yyyy-MM-dd HH:mm:ss.SSS

  • для типа параметра java.sql.Date формат значения берётся из DateDatatype (по умолчанию yyyy-MM-dd)

  • для java.sql.Time соответствующим datatype является TimeDatatype, значение по умолчанию: HH:mm:ss

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

Метод возвращает JSON-массив со списком извлеченных экземпляров сущности:

[
  {
    "_entityName": "sales_Order",
    "_instanceName": "00002",
    "id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
    "date": "2016-12-31",
    "description": "New Year party set",
    "number": "00002",
    "lines": [
      {
        "_entityName": "sales_OrderLine",
        "_instanceName": "Jack Daniels",
        "id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
        "price": 50.7,
        "name": "Jack Daniels"
      },
      {
        "_entityName": "sales_OrderLine",
        "_instanceName": "Hennessy X.O",
        "id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
        "price": 79.9,
        "name": "Hennessy X.O"
      }
    ],
    "customer": {
      "_entityName": "sales_Customer",
      "_instanceName": "Morgan Collins",
      "id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
      "firstName": "Morgan",
      "lastName": "Collins"
    }
  }
]

Список других возможных параметров для метода выполнения запросов можно посмотреть в Swagger документации.

5.8. Выполнение JPQL-запроса (POST)

JPQL-запрос также может быть выполнен с помощью POST-запроса. Это необходимо для случая, когда параметр JPQL-запроса является коллекцией. В файле конфигурации JPQL-запросов для REST API тип параметра-коллекции должен заканчиваться символами []: java.lang.String[], java.util.UUID[] и т.п.

<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
    <query name="ordersByIds" entity="sales_Order" view="order-edit">
        <jpql><![CDATA[select o from sales_Order o where o.id in :ids and o.status = :status]]></jpql>
        <params>
            <param name="ids" type="java.util.UUID[]"/>
            <param name="status" type="java.lang.String"/>
        </params>
    </query>
</queries>

Параметры JPQL-запроса должны быть переданы в теле HTTP-запроса в JSON map:

{
  "ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-f316-a7c8-7a96-87ba06afbbbd"],
  "status": "ready"
}

URL POST-запроса:

http://localhost:8080/app/rest/v2/queries/sales_Order/ordersByIds?returnCount=true

5.9. Вызов метода сервиса (GET)

Предположим, в системе имеется сервис OrderService, реализация которого выглядит следующим образом:

package com.company.sales.service;

import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.math.BigDecimal;

@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {

    @Inject
    private Persistence persistence;

    @Override
    public BigDecimal calculatePrice(String orderNumber) {
        BigDecimal orderPrice = null;
        try (Transaction tx = persistence.createTransaction()) {
            EntityManager em = persistence.getEntityManager();
            orderPrice = (BigDecimal) em.createQuery("select sum(oi.price) from sales_OrderLine oi where oi.order.number = :orderNumber")
                    .setParameter("orderNumber", orderNumber)
                    .getSingleResult();
            tx.commit();
        }

        return orderPrice;
    }
}

Перед выполнением метода сервиса с помощью REST API необходимо разрешить его вызов в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл rest-services.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties):

cuba.rest.servicesConfig = +com/company/sales/rest-services.xml

Содержимое файла rest-services.xml:

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="sales_OrderService">
        <method name="calculatePrice">
            <param name="orderNumber"/>
        </method>
    </service>
</services>

Для вызова метода сервиса, необходимо выполнить GET http-запрос к REST API вида:

http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?orderNumber=00001

Части URL:

  • sales_OrderService - имя сервиса

  • calculatePrice - имя метода сервиса

  • orderNumber - аргумент метода со значением

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

Метод сервиса может вернуть как простой тип данных, так и сущность, коллекцию сущностей или произвольный POJO. В нашем случае метод возвращает BigDecimal, поэтому в теле ответа нам вернется число:

39.2

5.10. Вызов метода сервиса (POST)

REST API позволяет выполнять методы сервисов, аргументами которых являются не только простые типы, но также:

  • сущности

  • коллекции сущностей

  • произвольные сериализуемые POJO

Небольшой пример. Предположим, в сервис OrderService, созданный в предыдущем разделе, добавлен следующий метод:

@Override
public OrderValidationResult validateOrder(Order order, Date validationDate){
    OrderValidationResult result=new OrderValidationResult();
    result.setSuccess(false);
    result.setErrorMessage("Validation of order "+order.getNumber()+" failed. validationDate parameter is: "+validationDate);
    return result;
}

Класс OrderValidationResult выглядит следующим образом:

package com.company.sales.service;

import java.io.Serializable;

public class OrderValidationResult implements Serializable {

    private boolean success;

    private String errorMessage;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

Новый метод сервиса принимает сущность Order в качестве первого аргумента и возвращает POJO.

Перед вызовом данного метода с помощью REST API необходимо разрешить его, добавив запись в конфигурационный файл rest-services.xml (его создание было рассмотрено в Вызов метода сервиса (GET)):

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="sales_OrderService">
        <method name="calculatePrice">
            <param name="orderNumber"/>
        </method>
        <method name="validateOrder">
            <param name="order"/>
            <param name="validationDate"/>
        </method>
    </service>
</services>

Метод validateOrder сервиса вызывается POST запросом по адресу:

http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder

Параметры в случае POST передаются в теле запроса в JSON объекте. Каждое поле JSON объекта соответствует аргументу метода сервиса:

{
  "order" : {
    "number": "00050",
    "date" : "2016-01-01"
  },
  "validationDate": "2016-10-01"
}

Значение параметра должно быть передано в формате, определённого для соответствующего datatype. Например:

  • если тип параметра java.util.Date, то формат значения берётся из DateTimeDatatype. По умолчанию это yyyy-MM-dd HH:mm:ss.SSS

  • для типа параметра java.sql.Date формат значения берётся из DateDatatype (по умолчанию yyyy-MM-dd)

  • для java.sql.Time соответствующим datatype является TimeDatatype, значение по умолчанию: HH:mm:ss

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

Метод вернет сериализованный POJO:

{
  "success": false,
  "errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}

5.11. Скачивание файлов

При скачивании файла передавать токен в заголовке запроса часто оказывается неудобно. Хочется иметь URL для скачивания, который можно подставить например в атрибут src тега img. В этом случае передача OAuth-токена возможна в самом запросе в параметре с именем access_token.

Например, в систему загружено изображение. Идентификатор соответствующего FileDescriptor - 44809679-e81c-e5ae-dd81-f56f223761d6.

В этом случае URL для загрузки изображения будет выглядеть так:

http://localhost:8080/app/rest/v2/files/44809679-e81c-e5ae-dd81-f56f223761d6?access_token=a2f0bb4e-773f-6b59-3450-3934cbf0a2d6

5.12. Загрузка файлов

Для загрузки файлов вам нужно получить OAuth-токен, который понадобится в дальнейших запросах.

Используем простую форму для загрузки:

<form id="fileForm">
    <h2>Select a file:</h2>
    <input type="file" name="file" id="fileUpload"/>
    <br/>
    <button type="submit">Upload</button>
</form>

<h2>Result:</h2>
<img id="uploadedFile" src="" style="display: none"/>

Далее используем jQuery, чтобы получить JSON с объектом data, который, по сути, представляет собой новый FileDescriptor. Использовать загруженный файл мы можем по идентификатору созданного экземпляра FileDescriptor с токеном в качестве параметра:

$('#fileForm').submit(function (e) {
    e.preventDefault();

    var file = $('#fileUpload')[0].files[0];
    var url = 'http://localhost:8080/app/rest/v2/files?name=' + file.name; // send file name as parameter

    $.ajax({
        type: 'POST',
        url: url,
        headers: {
            'Authorization': 'Bearer ' + oauthToken // add header with access token
        },
        processData: false,
        contentType: false,
        dataType: 'json',
        data: file,
        success: function (data) {
            alert('Upload successful');

            $('#uploadedFile').attr('src',
                'http://localhost:8080/app/rest/v2/files/' + data.id + '?access_token=' + oauthToken); // update image url
            $('#uploadedFile').show();
        }
    });
});

5.13. Пример использования из JavaScript

В данном разделе приведен пример использования REST API v2 из JavaScript, выполняющегося на HTML-странице. На странице изначально отображается форма логина, а после успешного входа - соответствующее сообщение и список сущностей.

Для простоты, в данном примере для хранения файлов HTML/CSS/JavaScript используется каталог modules/web/web/VAADIN, так как соответствующий каталог развернутого веб-приложения используется для выдачи статических ресурсов. Поэтому никаких настроек на сервере Tomcat делать не требуется. Результирующий URL будет начинаться с http://localhost:8080/app/VAADIN, так что данный вариант не стоит использовать в реальном приложении, вместо этого создайте отдельное веб-приложение со своим контекстом.

Загрузите jQuery и Bootstrap и скопируйте в каталог modules/web/web/VAADIN вашего проекта. Создайте файлы customers.html и customers.js, так что содержимое каталога должно быть примерно таким:

bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js

Содержимое файла customers.html:

<html>
    <head>
        <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
        <link rel="stylesheet" href="bootstrap.min.css"/>
    </head>
    <body>
        <div style="width: 300px; margin: auto;">
            <h1>Sales</h1>

            <div id="loggedInStatus" style="display: none" class="alert alert-success">
                Logged in successfully
            </div>
            <div id="loginForm">
                <div class="form-group">
                    <label for="loginField">Login:</label>
                    <input type="text" class="form-control" id="loginField">
                </div>
                <div class="form-group">
                    <label for="passwordField">Password:</label>
                    <input type="password" class="form-control" id="passwordField">
                </div>
                <button type="submit" class="btn btn-default" onclick="login()">Submit</button>
            </div>

            <div id="customers" style="display: none">
                <h2>Customers</h2>
                <ul id="customersList"></ul>
            </div>
        </div>
        <script type="text/javascript" src="customers.js"></script>
    </body>
</html>

Содержимое файла customers.js:

var oauthToken = null;
function login() {
    var userLogin = $('#loginField').val();
    var userPassword = $('#passwordField').val();
    $.post({
        url: 'http://localhost:8080/app/rest/v2/oauth/token',
        headers: {
            'Authorization': 'Basic Y2xpZW50OnNlY3JldA==',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        dataType: 'json',
        data: {grant_type: 'password', username: userLogin, password: userPassword},
        success: function (data) {
            oauthToken = data.access_token;
            $('#loggedInStatus').show();
            $('#loginForm').hide();
            loadCustomers();
        }
    })
}

function loadCustomers() {
    $.get({
        url: 'http://localhost:8080/app/rest/v2/entities/sales_Customer?view=_local',
        headers: {
            'Authorization': 'Bearer ' + oauthToken,
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        success: function (data) {
            $('#customers').show();
            $.each(data, function (i, customer) {
                $('#customersList').append("<li>" + customer.name + " (" + customer.email + ")</li>");
            });
        }
    });
}

Имя пользователя и пароль из полей ввода передаётся на сервер POST-запросом с закодированными Base64 клиентскими именем и паролем в заголовке Authorization, как описано в разделе Получение OAuth токена. В случае успешной аутентификации, страница получает с сервера значение токена доступа, токен записывается в переменную oauthToken, скрывается блок loginForm и отображается блок loggedInStatus.

Чтобы отобразить список покупателей, на сервер отправляется запрос на получение списка экземпляров сущности sales_Customer, передавая значение oauthToken в заголовке Authorization.

Если запрос выполнен, блок customers отображается на экране, и маркированный список customersList заполняется элементами, содержащими значения атрибутов name и email сущности Customer.

rest js 1
rest js 2

5.14. Получение локализованных сообщений

REST API позволяет получить локализованные заголовки для сущностей, перечислений и их атрибутов.

Например, чтобы получить локализованные сообщения для сущности sec$User, необходимо выполнить следующий GET запрос:

http://localhost:8080/app/rest/v2/messages/entities/sec$User

OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer.

Явно указать локаль запроса можно с помощью http-заголовка Accept-Language.

Ответ будет выглядеть следующим образом:

{
  "sec$User": "User",
  "sec$User.active": "Active",
  "sec$User.changePasswordAtNextLogon": "Change Password at Next Logon",
  "sec$User.createTs": "Created At",
  "sec$User.createdBy": "Created By",
  "sec$User.deleteTs": "Deleted At",
  "sec$User.deletedBy": "Deleted By",
  "sec$User.email": "Email",
  "sec$User.firstName": "First Name",
  "sec$User.group": "Group",
  "sec$User.id": "ID",
  "sec$User.ipMask": "Permitted IP Mask",
  "sec$User.language": "Language",
  "sec$User.lastName": "Last Name",
  "sec$User.login": "Login",
  "sec$User.loginLowerCase": "Login",
  "sec$User.middleName": "Middle Name",
  "sec$User.name": "Name",
  "sec$User.password": "Password",
  "sec$User.position": "Position",
  "sec$User.substitutions": "Substitutions",
  "sec$User.timeZone": "Time Zone",
  "sec$User.timeZoneAuto": "Autodetect Time Zone",
  "sec$User.updateTs": "Updated At",
  "sec$User.updatedBy": "Updated By",
  "sec$User.userRoles": "User Roles",
  "sec$User.version": "Version"
}

Для получения списка локализованных сообщений для перечисления используется следующий запрос:

http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.entity.RoleType

Если из URL убрать часть с именем сущности или перечисления, то будут возвращены локализованные сообщения для всех сущностей или перечислений.

5.15. Примеры версионирования модели данных

Атрибут сущности переименован

Предположим, атрибут oldNumber сущности sales_Order был переименован в newNumber, а атрибут date был переименован в deliveryDate. В этом случае конфигурация трансформации будет выглядеть следующим образом:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.0" currentEntityName="sales_Order">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
        <renameAttribute oldName="date" currentName="deliveryDate"/>
    </transformation>
    ...
</transformations>

Если клиентскому приложению необходимо работать со старой версии сущности sales_Order, то приложение должно передать значение версии модели данных в параметре URL modelVersion:

http://localhost:8080/app/rest/v2/entities/sales_Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0

Будет возвращен следующий результат:

{
  "_entityName": "sales_Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

Видим, что ответ содержит атрибуты oldNumber и date, хотя сущность в последней версии приложения уже имеет переименованные атрибуты newNumber и deliveryDate.

Имя сущности изменено

Предположим, что в одном из следующих релизов приложения имя сущности sales_Order также было изменено. Новое имя сущности теперь sales_NewOrder.

Конфиг трансформации для версии 1.1 выглядит так:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.1" oldEntityName="sales_Order" currentEntityName="sales_NewOrder">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
    </transformation>
    ...
</transformations>

В дополнение к конфигу из предыдущего примера здесь появился атрибут oldEntityName. Он описывает имя сущности, действительное для версии модели данных 1.1. Атрибут currentEntityName описывает текущее имя сущности.

Хотя сущность с именем sales_Order более не существует, следующий запрос будет работать:

http://localhost:8080/app/rest/v2/entities/sales_Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1

Контроллер REST API поймет, что поиск должен быть осуществлен среди экземпляров сущности sales_NewOrder, и после того, как сущность с заданным ID будет найдена, имя сущности и имя атрибута newNumber будут заменены в JSON:

{
  "_entityName": "sales_Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

Клиентское приложение также может использовать старую версию модели данных для создания или изменения сущности.

Этот POST запрос использует старое имя сущности и имеет JSON в старом формате в теле запроса, однако, запрос будет работать:

http://localhost:8080/app/rest/v2/entities/sales_Order

{
  "_entityName": "sales_Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}
Атрибут должен быть удален из JSON

Иногда может возникнуть ситуация, когда в сущность был добавлен новый атрибут, но клиент, работающий со старой версией модели данных, не должен получать этот атрибут при запросе к сущности. В этом случае стандартный JSON трансформер может удалить атрибут из результата.

Конфиг трансформации для данного случая выглядит примерно так:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.5" currentEntityName="sales_Order">
        <toVersion>
            <removeAttribute name="discount"/>
        </toVersion>
    </transformation>
    ...
</transformations>

Описание трансформации здесь содержит тег toVersion с вложенной командой removeAttribute. Это значит, что при выполнении трансформации из текущей версии модели данных к определенной версии (например, при запросе списка сущностей) атрибут discount будет удален из JSON.

Если выполнить запрос к REST API без атрибута modelVersion, то атрибут discount будет возвращен.

http://localhost:8080/app/rest/v2/entities/sales_Order/c838be0a-96d0-4ef4-a7c0-dff348347f93

{
    "_entityName": "sales_Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "number": "00001",
    "discount": 50
}

Если указать modelVersion в запросе, то атрибут discount будет удален:

http://localhost:8080/app/rest/v2/entities/sales_Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1

{
    "_entityName": "sales_Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "oldNumber": "00001"
}
Использование кастомных трансформеров

Вы также можете создать и зарегистрировать свой собственный трансформер JSON. В качестве примера рассмотрим следующую ситуацию.

Сущность sales_OldOrder была переименована в sales_NewOrder. В сущности имеется поле orderDate с типом дата. В старой версии сущности это поле содержало часть со временем, в новой версии сущности часть со временем в поле отсутствует. Клиент REST API, запрашивающий сущность со старой версией модели данных 1.0 ожидает, что дата будет иметь часть со временем. Получается, что JSON трансформер должен изменить значение в JSON.

Так будет выглядеть конфигурация трансформации для данного случая:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">

    <transformation modelVersion="1.0" oldEntityName="sales_OldOrder" currentEntityName="sales_NewOrder">
        <custom>
            <fromVersion transformerBeanRef="sales_OrderJsonTransformerFromVersion"/>
            <toVersion transformerBeanRef="sales_OrderJsonTransformerToVersion"/>
        </custom>
    </transformation>

    ...
</transformations>

Конфигурация содержит элемент custom с вложенными элементами toVersion и fromVersion. Эти элементы содержат ссылки на бины, т.е. кастомный трансформер должен быть зарегистрирован как Spring bean.

Важная деталь: в кастомном трансформере возможно потребуется использовать бин платформы RestTransformations (он предоставляет доступ к трансформерам для других сущностей). Но бин RestTransformations зарегистрирован в Spring контексте сервлета REST API, а не в главном контексте веб-приложения. Это значит, что бин кастомного трансформера также должен быть зарегистрирован в контексте REST API.

Как это сделать. Во-первых, создайте файл rest-dispatcher-spring.xml в модуле web или portal (например в пакете com.company.test).

Затем зарегистрируйте этот файл в файле свойств app.properties соответствующего модуля.

cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml

Файл rest-dispatcher-spring.xml должен содержать определения бинов для кастомных трансформеров:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

    <bean name="sales_OrderJsonTransformerFromVersion" class="com.company.test.transformer.OrderJsonTransformerFromVersion"/>
    <bean name="sales_OrderJsonTransformerToVersion" class="com.company.test.transformer.OrderJsonTransformerToVersion"/>

</beans>

Исходный код бина sales_OrderJsonTransformerToVersion:

package com.company.test.transformer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.haulmont.restapi.transform.AbstractEntityJsonTransformer;
import com.haulmont.restapi.transform.JsonTransformationDirection;

public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer {

    public OrderJsonTransformerToVersion() {
        super("sales_NewOrder", "sales_OldOrder", "1.0", JsonTransformationDirection.TO_VERSION);
    }

    @Override
    protected void doCustomTransformations(ObjectNode rootObjectNode, ObjectMapper objectMapper) {
        JsonNode orderDateNode = rootObjectNode.get("orderDate");
        if (orderDateNode != null) {
            String orderDateNodeValue = orderDateNode.asText();
            if (!Strings.isNullOrEmpty(orderDateNodeValue))
                rootObjectNode.put("orderDate", orderDateNodeValue + " 00:00:00.000");
        }
    }
}

Данный трансформер находит элемент orderDate в JSON и изменяет значение элемента, добавляя к нему часть со временем.

Когда сущность sales_Order будет запрошена с версией модели данных 1.0, то JSON результат будет содержать сущность, поле orderDate которой содержит часть со временем, хотя текущий тип поля сущности - дата без времени.

Несколько слов о кастомных трансформерах. Они должны реализовывать интерфейс EntityJsonTransformer. Вы также можете унаследоваться от класса AbstractEntityJsonTransformer и переопределить его метод doCustomTransformations. Класс AbstractEntityJsonTransformer содержит функциональность стандартных трансформеров.

5.16. Поиск сущностей с фильтром

REST API даёт возможность искать экземпляры сущностей по определенным условиям.

Примеры мы рассмотрим на модели данных, состоящей из двух сущностей:

  • Author с двумя полями: lastName и firstName

  • Book с тремя полями: title (String), author (Author) и publicationYear (Integer)

Чтобы выполнить поиск с фильтром URL должен выглядеть следующим образом:

http://localhost:8080/app/rest/v2/entities/test_Book/search

Условия поиска должны быть переданы в параметре filter. filter - это JSON объект, который содержит набор условий поиска. Если поиск выполняется с помощью GET запроса, то параметр filter передается в URL.

Пример 1

Необходимо найти все книги, опубликованные в 2007 году с именем автора, начинающимся с "Alex". JSON фильтр для подобного условия будет выглядеть так:

{
    "conditions": [
        {
            "property": "author.firstName",
            "operator": "startsWith",
            "value": "Alex"
        },
        {
            "property": "publicationDate",
            "operator": "=",
            "value": 2007
        }
    ]
}

По умолчанию все критерии поиска применяются с условием И.

Данный пример также демонстрирует возможность использования вложенных свойств объекта (author.firstName).

Tip

Полный список всех поддерживаемых операторов, используемых в условиях поиска, можно посмотреть в Swagger документации.

Пример 2

Следующий пример показывает две вещи: как выполнять поиск с помощью POST запроса и как использовать группы ИЛИ. В случае POST все параметры должны быть переданы в JSON объекте в теле запроса. Условия поиска должны быть помещены в поле объекта с именем filter. Остальные параметры (имя view, количество выгружаемых сущностей и т.п.) должны быть помещены в поля объекта с соответствующими именами:

{
  "filter": {
    "conditions": [
      {
        "group": "OR",
        "conditions": [
          {
            "property": "author.lastName",
            "operator": "contains",
            "value": "Stev"
          },
          {
            "property": "author.lastName",
            "operator": "=",
            "value": "Dumas"
          }
        ]
      },
      {
        "property": "publicationDate",
        "operator": "=",
        "in": [2007, 2008]
      }
    ]
  },
  "view": "book-view"
}

В этом примере коллекция conditions содержит не только объекты с критериями поиска, но и группу ИЛИ (OR). Итоговое условие можно представить так:

((author.lastName contains Stev) OR (author.lastName = Duma) AND (publicationDate in [2007, 2008]))

Обратите внимание, что параметр view также передан в теле запроса.

6. Безопасность приложений, использующих REST API

REST API определяет свою собственную область действия - REST. Для пользователей, входящих в систему через REST API, необходимо сконфигурировать отдельный набор ролей, включающий разрешение cuba.restApi.enabled. Если этого не сделать, пользователи не смогут входить через REST.

6.1. Предыдущая реализация

Если ваш проект использует предыдущую версию CUBA или мигрирует на версию CUBA 7.2, то используется предыдущая реализация ролей и разрешений. Поэтому, если вы используете REST API в приложении, вы должны настроить подсистему безопасности(Roles/Access Groups) и всегда поддерживать ее в актуальном состоянии в рабочей системе, чтобы предотвратить доступ к конфиденциальным данным.

Необходимо придерживаться следующих правил при использовании REST API:

  • Всегда отключайте доступ к REST API для ролей и пользователей, которые не должны его использовать. Смотрите специфическое разрешение CUBA > REST API > Use REST API в экране редактирования ролей.

  • Используйте стратегию "запретить по-умолчанию": всегда назначайте роль DENYING пользователям, имеющим доступ к REST API. Модель проекта меняется со временем, легко пропустить добавление детализированных ограничений.

  • Всегда настраивайте ограничения групп доступа к сущностями, не являющимися общими, для пользователей, имеющим доступ к REST API.

  • Помните, что EntityManager не накладывает ограничения групп доступа, поэтому используйте DataManager в сервисах среднего слоя, доступных для REST API.

  • Помните, что ограничения атрибутов сущностей не накладываются ролью DENYING. Настраивайте разрешения для каждого атрибута сущности отдельно.

  • Задайте уникальный пароль клиента в режиме эксплуатации.

Если у вас возникли проблемы с настройкой детализированной защиты для вашего проекта, рассмотрите возможность реализации пользовательских методов вместо универсального REST API.

Приложение A: Свойства приложения

cuba.rest.allowedOrigins

Задает список хостов, которым разрешен доступ к REST API. Значения хостов должны быть разделены запятой.

Значение по умолчанию: *

Используется в блоках Web Client и Web Portal.

cuba.rest.anonymousEnabled

Разрешает анонимный доступ к REST API.

Значение по умолчанию: false

Используется в блоках Web Client и Web Portal.

cuba.rest.client.id

Задает идентификатор клиента REST API. Клиент - это не пользователь платформы, а приложение (какой-либо веб-портал или мобильный клиент), использующий REST API. Идентификатор и пароль клиента используются для базовой аутентификации при доступе к URL для получения токена.

Значение генерируется автоматически при добавлении аддона.

Используется в блоках Web Client и Web Portal.

cuba.rest.client.authorizedGrantTypes

Определяет список типов авторизации (grant type), поддерживаемых клиентом по умолчанию. Для отключения поддержки refresh-токенов, удалите элемент refresh_token из значения свойства.

Значение по умолчанию: password,external,refresh_token

Используется в блоках Web Client и Web Portal.

cuba.rest.client.secret

Задает пароль клиента REST API. Клиент - это не пользователь платформы, а приложение (какой-либо веб-портал или мобильный клиент), использующий REST API. Идентификатор и пароль клиента используются для базовой аутентификации при доступе к URL для получения токена. Значение этого свойства помимо непосредственно пароля клиента REST API (например, secret) должно содержать и идентификатор используемого PasswordEncoder (например, {noop}).

Если пароль используется для базовой аутентификации, идентификатор PasswordEncoder прописывать не нужно.

Значение генерируется автоматически при добавлении аддона.

Используется в блоках Web Client и Web Portal.

cuba.rest.client.tokenExpirationTimeSec

Задает время жизни access токена REST API в секундах для клиента по умолчанию.

Значение по умолчанию: 43200 (12 часов)

Используется в блоках Web Client и Web Portal.

cuba.rest.client.refreshTokenExpirationTimeSec

Задает время жизни refresh токена REST API в секундах для клиента по умолчанию.

Значение по умолчанию: 31536000 (365 дней)

Используется в блоках Web Client и Web Portal.

cuba.rest.deleteExpiredTokensCron

Задает выражение cron, определяющее расписание удаления истекших OAuth токенов из базы данных.

Значение по умолчанию: 0 0 3 * * ?

Используется в блоке Middleware.

cuba.rest.jsonTransformationConfig

Аддитивное свойство, задающее файл, содержащий конфигурацию JSON трансформеров, используемых REST API для случаев, когда клиентскому приложению требуются данные в формате определенной версии модели данных.

Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.

XSD файла доступна по адресу http://schemas.haulmont.com/restapi/7.2/rest-json-transformations.xsd.

Значение по умолчанию: отсутствует

Пример:

cuba.rest.jsonTransformationConfig = +com/company/sample/json-transformations.xml

Используется в блоках Web Client и Web Portal.

cuba.rest.maxUploadSize

Максимальный размер файла (в байтах), который может быть загружен с помощью REST API.

Значение по умолчанию: 20971520 (20 Mb)

Используется в блоках Web Client и Web Portal.

cuba.rest.optimisticLockingEnabled

Включает оптимистичную блокировку сущностей, реализующих интерфейс Versioned, если атрибут version передан в JSON.

Значение по умолчанию: false

Используется в блоках Web Client и Web Portal.

cuba.rest.requiresSecurityToken

Если установлен в true, то в JSON загружаемой из БД сущности включается дополнительный системный атрибут, и этот же атрибут ожидается от клиента при сохранении сущности. Подробнее см. Ограничения для атрибутов-коллекций.

Значение по умолчанию: false

Используется в блоках Web Client и Web Portal.

cuba.rest.responseViewEnabled

Если установлено значение true, то в запросе на создание или обновление сущности можно передавать параметр responseView с названием представления. В таком случае загружается созданная/обновленная сущность в соответствии с указанным представлением и возвращается в ответе.

Если установлено значение false, то ответ содержит JSON c полным графом созданной/обновленной сущности.

Значение по умолчанию: true

Используется в блоках Web Client и Web Portal.

cuba.rest.reuseRefreshToken

Определяет, должен ли refresh-токен быть повторно использован. Если установить значение в false, то когда access-токен запрашивается с помощью refresh-токена, то будет выдан новый refresh-токен, а старый будет удалён.

Значение по умолчанию: true

Используется в блоках Web Client и Web Portal.

cuba.rest.servicesConfig

Аддитивное свойство задающее файл, который содержит список доступных для вызова через REST API сервисов.

Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.

XSD файла доступна по адресу http://schemas.haulmont.com/restapi/7.2/rest-services-v2.xsd.

Значение по умолчанию: не задано

Пример:

cuba.rest.servicesConfig = +com/company/sample/app-rest-services.xml

Используется в блоках Web Client и Web Portal.

cuba.rest.storeTokensInDb

Включает хранение OAuth токенов в базе данных. По умолчанию токены хранятся только в памяти.

Хранится в базе данных.

Интерфейс: RestConfig

Значение по умолчанию: false

Используется в блоке Middleware.

cuba.rest.syncTokenReplication

Включает синхронную репликацию созданных токенов между кластерами Middleware. По умолчанию токены отправляются в кластер асинхронно.

Хранится в свойствах приложения.

Интерфейс: RestConfig

Значение по умолчанию: false

Используется в блоке Middleware.

cuba.rest.tokenMaskingEnabled

Определяет, должны ли токены REST API быть маскированы в логах приложения.

Значение по умолчанию: true

Используется в блоках Web Client и Web Portal.

cuba.rest.queriesConfig

Аддитивное свойство задающее файл, который содержит список доступных для выполнения через REST API JPQL запросов.

Файл загружается с помощью интерфейса Resources, поэтому может быть расположен в classpath или в конфигурационном каталоге.

XSD файла доступна по адресу http://schemas.haulmont.com/restapi/7.2/rest-queries.xsd.

Значение по умолчанию: не задано

Пример:

cuba.rest.queriesConfig = +com/company/sample/app-rest-queries.xml

Используется в блоках Web Client и Web Portal.