Платформа CUBA. Руководство по разработке приложений Manual

User Manual: Pdf

Open the PDF directly: View PDF PDF.
Page Count: 855

DownloadПлатформа CUBA. Руководство по разработке приложений Manual
Open PDF In BrowserView PDF
Платформа CUBA.
Руководство по
разработке приложений
Версия 6.7

Предисловие
Данное руководство содержит справочную информацию
по платформе CUBA и охватывает наиболее важные темы
разработки бизнес-приложений на платформе.
Для успешной работы с платформой требуется знание
следующих технологий:
Java Standard Edition
Реляционные базы данных (SQL, DDL)
Для глубокого понимания принципов работы платформы
полезным является знакомство со следующими
технологиями и фреймворками:
Gradle build system
Spring Framework
Java Persistence API
Vaadin web applications framework
HTML / CSS / JavaScript
Java Servlets
Настоящее руководство, а также другая документация по
платформе CUBA, доступны по адресу www.cubaplatform.ru/manual. Обучающие видео-материалы и
презентации располагаются по адресу www.cuba-

презентации располагаются по адресу www.cubaplatform.ru/tutorials. Онлайн демо-приложения могут быть
запущены со страницы www.cuba-platform.ru/online-demo.
Если у Вас имеются предложения по улучшению данного
руководства, обратитесь пожалуйста в службу
поддержки по адресу www.cuba-platform.ru/support. При
обнаружении ошибки в документации укажите,
пожалуйста, номер главы и приведите небольшой участок
окружающего текста для облегчения поиска.

1. Введение
В данной главе приводятся сведения о назначении и
возможностях платформы CUBA.

1.1. Обзор платформы
Платформа CUBA предназначена для разработки
корпоративных решений, характеризующихся сложной
моделью данных, десятками или сотнями экранов,
большим количеством бизнес-логики, а также
требованиями к контролю прав доступа,
масштабируемости и отказоустойчивости.
Широкий набор готовой функциональности, развитые
средства генерации кода, визуальный дизайнер
интерфейсов, а также поддержка hot deploy радикально
сокращают время и стоимость разработки решения.
Платформа полностью построена на стеке открытых Java
технологий, что позволяет использовать наработки
крупнейшей экосистемы свободного ПО в мире, а также
контролировать исходный код всего стека.
Открытая архитектура позволяет переопределять
поведение большинства механизмов платформы,

поведение большинства механизмов платформы,
обеспечивая высокую гибкость. Разработчики могут
использовать популярные Java IDE и имеют полный доступ
к исходному коду.
Приложения на платформе легко встраиваются в ИТинфраструктуру благодаря поддержке основных баз
данных и серверов приложений, а также возможности
работы в облаке. Платформа позволяет обеспечить
отказоустойчивость и масштабируемость решений и
предоставляет средства интеграции с внешними
системами.

1.2. Технические требования
Минимальные требования для ведения разработки на
платформе CUBA:
Оперативная память - 4 ГБ.
Место на жестком диске - 5 ГБ.
Операционная система - Microsoft Windows, Linux или
macOS.

1.3. Release Notes
Список изменений в платформе доступен по адресу
http:// les.cuba-platform.com/cuba/release-notes/6.7

2. Установка и настройка
инструментария
Минимально необходимым набором программного
обеспечения является:
Java SE Development Kit (JDK) 8

Установите Java SE Development Kit (JDK) 8 и проверьте

Установите Java SE Development Kit (JDK) 8 и проверьте
его работоспособность, выполнив в консоли команду
java -version
В ответ должно быть выведено сообщение с номером
версии Java, например 1.8.0_152.
Java 9 пока не поддерживается. Собирать и
запускать CUBA-приложения можно только на Java 8.
Для сборки и запуска проектов вне Studio в переменной
окружения JAVA_HOME необходимо установить путь к
корневому каталогу JDK, например C:\Program
Files\Java\jdk1.8.0_152.
Для Windows это можно сделать, открыв Компьютер →
Свойства системы → Дополнительные параметры
системы → Дополнительно → Переменные среды, и
задав значение переменной в списке Системные
переменные.
Для macOS JAVA_HOME рекомендуется задать в
~/.bash_profile:
export JAVA_HOME=$(/usr/libexec/java_home -v
1.8)
Если вы установили Java 9 на macOS, CUBA Studio и
приложения перестанут запускаться, независимо от
значения JAVA_HOME. В этом случае обратитесь к
информации в разделе решение проблем ниже.
Cреда разработки на Java

IntelliJ IDEA или Eclipse. Рекомендуется использовать IntelliJ
IDEA (Community или Ultimate).
База данных

В простейшем случае в качестве сервера баз данных
приложений используется встроенный HyperSQL
(http://hsqldb.org), что вполне подходит для исследования

возможностей платформы и прототипирования
приложений. Для создания реальных приложений
рекомендуется установить и использовать в проекте
какую-либо из полноценных СУБД, поддерживаемых
платформой, например PostgreSQL.
Веб-браузер

Веб-интерфейс приложений, создаваемых на основе
платформы, поддерживает все популярные
современные браузеры, в том числе Google Chrome, Mozilla
Firefox, Safari, Opera 15+, Internet Explorer 9+ , Microsoft Edge.
Решение проблем
1.

Если по какой-то причине вы установили Java 9 на
macOS, CUBA Studio и приложения перестанут
запускаться. Для того, чтобы восстановить
работоспособность, выполните следующее:
a.

Сделайте инсталляцию JDK 9 не используемой по
умолчанию в системе: переименуйте файл
/Library/Java/JavaVirtualMachines/jdk9.0.1.jdk/Contents/Info.plist в
Info.plist.disabled (замените jdk-9.0.1.jdk
реальной версией вашего JDK 9).

b.

Замените JavaAppletPlugin.plugin, установленный
Java 9 на плагин из Java 8:
Удалите или переименуйте /Library/Internet
Plug-Ins/JavaAppletPlugin.plugin
Установите JDK 8 снова, при этом плагин будет
переустановлен.

c.

Убедитесь что вы указали получение JAVA_HOME с
аргументом -v 1.8, как показано выше.
Перелогиньтесь или выполните source
.bash_profile после внесения изменений.

2.

Убедитесь, что ваше окружение нe содержит
переменных CATALINA_HOME, CATALINA_BASE и

переменных CATALINA_HOME, CATALINA_BASE и
CLASSPATH. Эти переменные могут вызвать проблемы с
запуском веб-сервера Apache Tomcat, который
используется во время разработки. Перезагрузите
компьютер после удаления переменных.

2.1. Установка CUBA Studio
Окружение

Убедитесь в том, что на компьютере установлен Java SE
Development Kit (JDK) 8, выполнив в консоли команду
java -version
В ответ должно быть выведено сообщение с номером
версии Java, например 1.8.0_152.
Если вы используете OpenJDK на Linux, установите
OpenJFX, например:
sudo apt-get install openjfx
Если для соединения с интернетом используется
прокси-сервер, в JVM, исполняющие Studio и Gradle,
необходимо передавать специальные системные
свойства Java. Они описаны в документе
http://docs.oracle.com/javase/8/docs/technotes/guides/net/pro
xies.html (см. свойства для протоколов HTTP и HTTPS).
Рекомендуется установить нужные свойства в
переменной окружения JAVA_OPTS. Скрипт запуска
Studio передает JAVA_OPTS в java.exe.
Новая установка Studio
1.

Загрузите подходящий инсталлятор или ZIP архив со
страницы https://www.cuba-platform.ru/download.

2.

Запустите инсталлятор или распакуйте ZIP архив в
локальный каталог, например, c:\work\studio.

3.

Запустите установленное приложение или откройте
командную строку, перейдите в подкаталог bin и
запустите studio.bat или studio в зависимости от

запустите studio.bat или studio в зависимости от
вашей операционной системы.
4.

В окне CUBA Studio Server введите следующие
параметры:
Server port − порт, на котором будет запущен сервер
CUBA Studio (по умолчанию 8111).
Remote connection - по умолчанию Studio принимает
соединения только с локального компьютера.
Установите данный флажок, если вам нужна
возможность подключения к этому экземпляру Studio
с удаленного хоста.
Silent startup - если выбрано, сервер Studio запускается
в трее и открывает UI в браузере автоматически.
Данная опция доступна только в Windows.

5.

Запустите сервер Studio, нажав кнопку Start.
Когда запустится веб-сервер, в поле URL отобразится
адрес, по которому доступен интерфейс Studio. Нажав
→, можно открыть веб-браузер, нажав Copy −
скопировать адрес в буфер обмена.

6.

Запустите веб-браузер и перейдите по указанному
адресу. В веб-интерфейсе Studio перейдите на вкладку
Settings и введите следующие параметры:
Java home − JDK, который будет использоваться для
сборки и запуска проектов. Если вы установили
переменную окружения JAVA_HOME как описано в
начале данной главы, ее значение будет
подставлено в данное поле. В противном случае
Studio попытается самостоятельно найти каталог

Studio попытается самостоятельно найти каталог
установки Java.
Gradle home - путь к Gradle. Оставьте поле пустым, в
этом случае при первом запуске будет
автоматически загружен нужный дистрибутив
Gradle.
Если по какой-либо причине Вы хотите использовать
уже установленный на компьютере Gradle, введите в
поле путь к соответствующему каталогу. Текущая
версия системы сборки проектов протестирована на
Gradle 3.4.
IDE port − порт, на котором принимает подключения
плагин IDE (по умолчанию 48561).
O ine - включить возможность работы без интернетсоединения при условии, что все необходимые
библиотеки были предварительно загружены из
репозитория.
Check for updates - проверять наличие новых версий
при старте.
Send anonymous statistics and crash reports - разрешить
Studio отправлять статистику ошибок
разработчикам.
Help language - язык встроенной справки.
Logging level - уровень логирования: TRACE, DEBUG, INFO,
WARN, ERROR или FATAL. По умолчанию INFO.

7.

Нажмите Apply and proceed to projects.

8.

Нажмите Create new для создания нового проекта, или
Import для добавления имеющегося проекта в список
Recent.

9.

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

Обновление CUBA Studio

Если вы обноляете Studio на новую bug- x версию
(например с 6.5.0 на 6.5.1), устанавливайте ее в
существующий каталог, например на Windows это будет
C:\Program Files (x86)\CUBA Studio 6.5. При
установке новой minor или major версии, используйте
отдельный каталог, например CUBA Studio 6.6.
Если Studio установлена из инсталлятора Windows EXE или
из ZIP-архива, она поддерживает авто-обновление на
новые bug- x релизы. Файлы обновлений сохраняются в
каталоге ~/.haulmont/studio/update. В случае какихлибо проблем с новой версией вы можете просто
удалить файлы обновления и Studio вернется к версии,
установленной ранее вручную.
Автообновление не работает для minor и major релизов, и
если Studio была установлена из macOS DMG. В этом
случае загрузите и запустите новый инсталлятор
вручную.

2.2. Интеграция CUBA Studio с IDE

2.2. Интеграция CUBA Studio с IDE
Для интеграции с IntelliJ IDEA или Eclipse выполните
следующие шаги:
1.

Откройте или создайте новый проект в Studio.

2.

Перейдите в секцию Project properties и нажмите кнопку
Edit. Выберите нужную Java IDE флажками IntelliJ IDEA или
Eclipse.

3.

В главном меню Studio выберите пункт меню Build →
Create or update  project les. В каталоге проекта будут
созданы соответствующие файлы.

4.

Для интеграции с IntelliJ IDEA:
a.

5.

Запустите IntelliJ IDEA 13+ и установите плагин CUBA
Framework Integration, доступный в репозитории
плагинов: File > Settings > Plugins > Browse Repositories.

Для интеграции с Eclipse:
a.

Запустите Eclipse 4.3+, откройте Help > Install New
Software, добавьте репозиторий http://files.cubaplatform.com/eclipse-update-site и установите
плагин CUBA Plugin.

b.

В Eclipse в меню Window > Preferences в секции CUBA
установите флажок Studio Integration Enabled и нажмите
на кнопку OK.

Обратите внимание, что в панели статуса Studio
загорелась надпись IDE: on port 48561. Теперь при нажатии
кнопок IDE в Studio соответствующие файлы исходных
кодов будут открываться редактором IDE.

3. Быстрый старт
В данном разделе рассматривается создание приложения
при помощи CUBA Studio. Эта же информация изложена в
видеороликах, доступных по адресу www.cuba-

видеороликах, доступных по адресу www.cubaplatform.ru/quickstart.
На Вашей рабочей машине уже должно быть установлено
и настроено необходимое программное обеспечение, см.
Установка и настройка инструментария.
Основные задачи, стоящие при разработке нашего
приложения:
1.

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

2.

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

3.1. Описание задачи
Приложение предназначено для ведения сведений о
покупателях и их заказах.
Покупатель имеет следующие характеристики:
Имя
Электронная почта
Характеристики заказа:
Принадлежность покупателю
Дата
Сумма

Пользовательский интерфейс приложения должен
содержать:

содержать:
Окно списка покупателей;
Окно редактирования сведений о покупателе,
содержащее также список заказов данного покупателя;
Окно общего списка заказов;
Окно редактирования заказа.

3.2. Создание проекта
1.

Запустите CUBA Studio и откройте ее веб-интерфейс (см.
Установка CUBA Studio).

2.

Нажмите на кнопку Create new.

3.

В окне New project в поле Project name введите имя
проекта - sales. Имя должно содержать только
латинские буквы, цифры и знак подчеркивания.
Тщательно продумайте имя проекта на данном этапе,
так как в дальнейшем изменить его будет достаточно
сложно.

4.

В полях ниже автоматически сгенерируются:
Project path − путь к каталогу нового проекта. Каталог
можно выбрать вручную, нажав на кнопку … рядом с
полем. Отобразится окно Folder select со списком папок
на жестком диске. Вы можете выбрать одну из них или
создать новый каталог, нажав на кнопку +.
Project namespace - пространство имен, которое будет
использоваться как префикс имен сущностей и
таблиц базы данных. Пространство имен может
состоять только из латинских букв, и должно быть как
можно короче. Например, если имя проекта - sales_2,
то пространство имен может быть sales или sal.
Root package − корневой пакет Java-классов. Может
быть скорректирован позже, однако сгенерированные
на этапе создания классы перемещены не будут.
Repository − URL и параметры аутентификации

Repository − URL и параметры аутентификации
репозитория бинарных артефактов.
Platform version - используемая в проекте версия
платформы. Артефакты платформы будут
автоматически загружены из репозитория при сборке
проекта.

5.

Нажмите на кнопку OK. В указанном каталоге sales
будет создан пустой проект, и откроется главное окно
Studio.

6.

Сборка проекта. Выберите пункт главного меню Studio
Build → Assemble project. На этом этапе будут загружены
все необходимые библиотеки, и в подкаталогах build
модулей будут собраны артефакты проекта.

7.

Создание базы данных на локальном сервере HyperSQL.
Выберите пункт меню Run → Create database. Имя БД по
умолчанию совпадает с пространством имен проекта.

8.

Выберите пункт меню Run → Deploy. В подкаталоге
deploy проекта будет установлен сервер Tomcat с
собранным приложением.

9.

Выберите пункт меню Run → Start application server . Через
несколько секунд в панели статуса ссылка рядом с
надписью Web application станет доступной, и по ней
можно осуществить переход к приложению
непосредственно из Studio.

непосредственно из Studio.
Логин и пароль пользователя − admin / admin.
Запущенное приложение содержит два главных пункта
меню (Administration и Help), функциональность
подсистемы безопасности и администрирования
системы.

3.3. Создание сущностей
Создадим класс сущности Customer (Покупатель).
Перейдите на вкладку Data Model на панели навигатора
и нажмите на кнопку New > Entity. Появится диалоговое
окно New entity.
В поле Class name введите название класса сущности −
Customer.

Нажмите OK. В рабочей области откроется страница
дизайнера сущности.

В полях Name и Table автоматически сгенерируются имя
сущности и имя таблицы в базе данных.
В поле Parent class оставьте установленное значение −
StandardEntity.
Поле Inheritance strategy оставьте пустым.
Далее создадим атрибуты сущности. Для этого нажмите
на кнопку New, находящуюся под таблицей Attributes.
В отобразившемся окне Create attribute в поле Name
введите название атрибута сущности − name, в списке
Attribute type выберите значение DATATYPE, в поле Type
укажите тип атрибута String и далее укажите длину
текстового атрибута в поле Length, равной 100 символам.
Установите флажок Mandatory. В поле Column
автоматически сгенерируется имя колонки таблицы в
базе данных.

Для добавления атрибута нажмите на кнопку Add.
Атрибут email создается таким же образом, за
исключением того, что в поле Length следует указать
значение 50.
После создания атрибутов перейдите на вкладку Instance
Name дизайнера сущности для задания Name pattern. В
списке Available attributes выделите атрибут name и
перенесите его в список Name pattern attributes, нажав на
кнопку с изображением стрелки вправо.

На этом создание сущности Customer завершено. Нажмите
на кнопку OK в верхней панели для сохранения
изменений.
Создадим сущность Order (Заказ). В панели DATA MODEL
нажмите на кнопку New > Entity. В поле Class name введите
название класса сущности − Order. Сущность должна
иметь следующие атрибуты:
Name − customer, Attribute type − ASSOCIATION, Type −
Customer, Cardinality − MANY_TO_ONE.
Name − date, Attribute type − DATATYPE, Type − Date. Для
атрибута date установите флажок Mandatory.
Name − amount, Attribute type − DATATYPE, Type − BigDecimal.

3.4. Создание таблиц базы данных
Для создания таблиц базы данных достаточно на вкладке
Data Model панели навигатора нажать на кнопку Generate DB
scripts. После этого откроется страница Database Scripts. На
вкладке будут сгенерированы скрипты обновления базы
данных от ее текущего состояния (UPDATE SCRIPTS) и
скрипты создания базы данных с нуля (INIT TABLES, INIT
TABLES, INIT DATA). Также на вкладке будут доступны уже
выполненные скрипты обновления базы данных, если они
есть.

Чтобы сохранить сгенерированные скрипты, нажмите на
кнопку Save and close. Для запуска скриптов обновления
остановите запущенное приложение с помощью команды
Run → Stop application server, затем выполните Run → Update
database.

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

3.5.1. Экраны управления Покупателями
Для создания стандартных экранов просмотра и
редактирования покупателей необходимо выделить
сущность Customer на вкладке Data Model панели
навигатора и нажать на кнопку New > Generic UI screen внизу
панели. После этого на экране отобразится страница
создания стандартных экранов сущности.
В списке доступных шаблонов выберите Entity browser and
editor screens.

Все поля этого окна заполнены значениями по умолчанию,
пока не будем их менять. Нажмите на кнопку Create и
затем Close.
Во вкладке Generic UI панели навигатора в модуле Web
Module появятся элементы customer-browse.xml и
customer-edit.xml.

3.5.2. Экраны управления Заказами
Сущность Order (Заказ) имеет следующую особенность:
так как среди прочих атрибутов существует ссылочный
атрибут Order.customer, требуется определить
представление сущности Order, включающее этот
атрибут (стандартное представление _local не включает
ссылочных атрибутов).
Для этого перейдите на вкладку Data Model на панели
навигатора, выделите сущность Order и выберите New >
View. Отобразится страница дизайнера представлений. В
качестве имени введите order-with-customer, в списке
атрибутов нажмите на атрибут customer и на
отобразившейся справа панели выберите представление
_minimal для сущности Customer.

Нажмите на кнопку OK в верхней панели.
Далее выделите сущность Order и выберите New > Generic
UI screen.
В отобразившемся окне выберите представление orderwith-customer в полях View для браузера и редактора и
нажмите на кнопку Create и, затем, Close.

Во вкладке Generic UI панели навигатора в модуле Web
Module появятся элементы order-edit.xml и orderbrowse.xml.

3.5.3. Меню приложения

3.5.3. Меню приложения
При создании экраны были добавлены в пункт меню
application, имеющийся по умолчанию. Переименуем его.
Для этого перейдите на вкладку Generic UI на панели
навигатора и нажмите на ссылку Open web menu .
Отобразится страница дизайнера меню. Выделите пункт
меню application для просмотра его свойств.
В поле Id введите новое значение идентификатора меню −
shop. После редактирования меню нажмите на кнопку OK в
верхней панели.

3.5.4. Экран редактирования Покупателя со
списком Заказов
Займемся задачей отображения списка заказов в окне
редактирования покупателя.
Перейдите на вкладку Generic UI на панели навигатора.
Выделите экран customer-edit.xml и нажмите на
кнопку Edit.
На странице дизайнера экрана перейдите на вкладку
Datasources и нажмите на кнопку New.

Datasources и нажмите на кнопку New.
Выделите только что созданный источник данных в
списке. В правой части страницы отобразятся его
характеристики.
В поле Type укажите collectionDatasource.
В списке Entity выберите сущность Order.
В поле Id будет автоматически заполнено значение
идентификатора источника данных − ordersDs.
В списке View выберите представление _local.
В поле Query введите следующий запрос:
select e from sales$Order e where e.customer.id =
:ds$customerDs order by e.date

Здесь запрос содержит условие отбора Заказов с
параметром ds$customerDs. Значением параметра с
именем вида ds${datasource_name} будет
идентификатор сущности, установленной в данный
момент в источнике данных datasource_name, в данном
случае − идентификатор редактируемого Покупателя.

Нажмите на кнопку Apply для сохранения изменений.
Далее перейдите на вкладку Layout в дизайнере экрана
и в палитре компонентов найдите компонент Label.
Перетащите этот компонент на панель иерархии
компонентов экрана, между fieldGroup и
windowActions. Перейдите на вкладку Properties на
панели свойств. В поле value введите значение
компонента: Orders.

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

рядом с полем value, чтобы создать новое

сообщение msg://orders и задать его значение на
требуемых языках.
Перетащите компонент Table из палитры компонентов
на панель иерархии компонентов между label и
windowActions. Выделите компонент в иерархии и
перейдите на вкладку Properties. Задайте размеры

таблицы: в поле width укажите 100%, в поле height
установите значение 200px. Из списка доступных
источников данных выберите orderDs, после этого в
поле id с помощью кнопки
сгенерируйте
идентификатор таблицы: ordersTable.

Для сохранения изменений в экране редактирования
Покупателя нажмите на кнопку OK в верхней панели.

3.6. Запуск приложения
Посмотрим, как созданные нами экраны выглядят в
работающем приложении. Для этого выполните Run > Start
application server.
Зайдите в систему, использовав стандартные имя и
пароль в окне логина. Откройте пункт меню Shop >
Customers:

Рисунок 1. Экран списка Customers

Нажмите на кнопку Create и создайте нового покупателя:

Рисунок 2. Экран редактирования Customer

Откройте пункт меню Shop > Orders:

Рисунок 3. Экран списка Orders

Нажмите на кнопку Create и создайте новый заказ, выбрав
в поле Customer только что созданного покупателя:

Рисунок 4. Экран редактирования Order

В таблице на экране редактирования покупателя теперь
отображается только что созданный заказ:

Рисунок 5. Экран редактирования Customer

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

момент можете перейти к другой секции или заняться
кодированием.
Большинство разделов снабжены демо-приложениями.
Вы можете увидеть их в работе онлайн, посмотреть
исходный код на GitHub, или загрузить и запустить
локально. Ссылки на проекты приложений доступны
также на вкладке Samples в Studio.
Данный раздел находится в работе и будет
постоянно дополняться.

4.1. Организация бизнес-логики
Один из первых вопросов, возникающих в начале
процесса разработки на платформе, это "где мне
расположить мою бизнес-логику"? Использование Studio
сильно облегчает создание модели данных и CRUD
экранов, любой реальный проект требует создания логики
помимо загрузки и сохранения данных. Данный раздел
содержит информацию о том, как эффективно
организовать бизнес-логику в зависимости от задачи.
Большинство примеров в данном разделе работают со
следующей моделью:

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

4.1.1. Бизнес-логика в контроллерах
Например, необходимо запускать расчет

LIVE DEMO

скидки, когда пользователь нажимает кнопку на экранебраузере заказчиков. В этом случае, наиболее простое
решение - это разместить логику расчета прямо в
контроллере экрана.
См. кнопку Calculate discount в демо-приложении и
реализацию контроллера: CustomerBrowse.java. Пожалуйста,
имейте в виду, что данная имплементацию расчета не
является оптимальной (см. варианты работы с данными в
разделе Загрузка и сохранение данных).
Данный подход приемлем, если логика вызывается из
одного места и она не слишком сложна, чтобы уместиться
в нескольких коротких методах.

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

LIVE DEMO

предыдущего раздела. Допустим, требуется вызывать
расчет из двух экранов: и из браузера, и из редактора.
Чтобы не дублировать код, нужно извлечь код из
контроллера и поместить в некоторое общедоступное
место. Это может быть управляемый бин клиентского
уровня.
Управляемый бин - это класс с аннотацией @Component. Он
может быть инжектирован в другие бины и контроллеры
экранов, или получен с помощью статического метода
AppBeans.get(). Если класс бина реализует некоторый
интерфейс, то к нему можно обращаться через этот
интерфейс.
Имейте в виду, что для того чтобы бин был доступен для
контроллеров экранов, он должен располагаться в одном
из следующих модулей: global, gui или web вашего проекта.
В случае global, бин будет также доступен на среднем
слое.
См. кнопку Calculate discount на экранах браузера и

См. кнопку Calculate discount на экранах браузера и
редактора в демо-приложении, и реализацию:

CustomerBrowse.java - контроллер браузера.
CustomerEdit.java - контроллер редактора.
DiscountCalculator.java - бин расчета скидок. Он использует
DataManager для загрузки списка заказов данного
заказчика из базы данных.

4.1.3. Использование сервисов среднего слоя
LIVE DEMO

В предыдущем разделе мы рассмотрели
инкапсуляцию бизнес-логики в бине клиентского уровня.
Теперь мы пойдем дальше и поместим нашу логику в
наиболее подходящее место: на средний слой. Сделав это,
мы достигнем следующих целей:
Наши бизнес-методы будут доступны клиентам всех
типов, включая Polymer UI.
Мы сможем использовать API, доступный только на
middleware: EntityManager, transactions, и т.п.
Чтобы вызвать бизнес-метод среднего слоя, необходимо
создать сервис. Studio может помочь в создании заготовки
сервиса:
Переключитесь на вкладку Middleware и нажмите New >
Service.
Измените имя интерфейса сервиса на DiscountService.
Имена класса и самого сервиса будут изменены
соответственно. Нажмите OK или Apply.

соответственно. Нажмите OK или Apply.
Нажмите IDE и откройте интерфейс сервиса в IDE.
Создайте новый метод и реализуйте его в классе
сервиса.
См. пример реализации в демо-приложении:

CustomerBrowse.java and CustomerEdit.java - контроллер
экрана, который вызывает сервис.
DiscountService.java - интерфейс сервиса.
DiscountServiceBean.java - класс реализации сервиса.
DiscountCalculator.java - бин среднего слоя,
рассчитывающий скидки. Разумеется, сервис мог бы
содержать бизнес-логику сам, но мы будем
использовать этот делегат для того чтобы разделять
логику с entity listener и JMX-бином (см. следующие
разделы).
Обратите внимание, что данный бин отличается от
рассмотренного в предыдущем разделе: он расположен
в модуле core и использует EntityManager для загрузки
суммы заказов из базы данных.
Теперь давайте сделаем наш бизнес-метод доступным
для внешних клиентов через REST API:
Откройте сервис на редактирование в Studio и
переключитесь на вкладку REST Methods.
Установите для метода флажок REST invocation allowed.

Studio создаст файл rest-services.xml и зарегистрирует
в нем метод. После перезапуска сервера вы сможете
вызвать метод с помощью HTTP-запросов. Например,
следующий GET-запрос должен работать с нашим онлайн
демо-сервером:
https://demo1.cuba-platform.com/businesslogic/rest/v2/services/sample_DiscountService/calculateDiscount?
customerId=1797f54d-5bec-87a6-4330-d958955743a2
Имейте в виду, что демо-приложение имеет анонимный
доступ. В большинстве реальных сценариев
использования необходимо будет аутентифицироваться,
прежде чем выполнять запросы.

4.1.4. Использование Entity Listeners
Entity listeners позволяют выполнять бизнеслогику каждый раз, когда сущность создается,

LIVE DEMO

изменяется или удаляется в базе данных. Например, мы
можем пересчитывать скидку для заказчика каждый раз,
когда некоторый заказ для него изменяется.
Заготовку для entity listener можно легко создать в Studio:
Перейдите на вкладку Middleware и нажмите New > Entity
listener.
Измените имя класса на OrderEntityListener и
включите флажки для интерфейсов
BeforeInsertEntityListener,
BeforeUpdateEntityListener и
BeforeDeleteEntityListener.
Выберите сущность Order в поле Entity type.
Нажмите OK или Apply и откройте класс listener в IDE.
См. пример в демо-приложении:

OrderEntityListener.java - класс entity listener.
DiscountCalculator.java - бин среднего слоя,
рассчитывающий скидки. Entity listener мог бы содержать
бизнес-логику сам, но мы использовуем этот делегат
для того, чтобы разделять логику с сервисом и JMX
бином.
Если вы откроете экран Logic in Entity Listeners демоприложения, вы увидите две таблицы: заказы и заказчики.
Создайте, измените или удалите заказ, обновите таблицу
заказчиков, и вы увидите, что скидка для
соответствующего заказчика изменилась.

4.1.5. Использование JMX-бинов
С помощью JMX-бинов можно предоставить

LIVE DEMO

доступ к некоторой административной
функциональности вашего приложения без создания
пользовательского интерфейса для нее. Данная
функциональность будет также доступна через
встроенную JMX-консоль и через внешние инструменты
JMX, например jconsole.
В нашем примере со скидками пользователь, имеющий
доступ к JMX-консоли, сможет пересчитывать скидки для
всех заказчиков или для заказчика с указанным id.
Studio на данный момент не умеет создавать заготовки
JMX-бинов, поэтому все классы и конфигурационные
элементы придется создавать вручную в IDE.
См. пример реализации в демо-приложении:

DiscountsMBean.java - интерфейс JMX-бина.
Discounts.java - реализация JMX-бина.
DiscountCalculator.java - бин среднего слоя, вызываемый
JMX-бином. JMX-бин мог бы содержать бизнес-логику
сам, но мы использовуем этот делегат для того, чтобы
разделять логику с entity listener и JMX бином.
spring.xml - в данном файле JMX-бин регистрируется.

4.1.6. Запуск кода на старте приложения
Иногда бывает необходимо выполнить некоторый код
сразу после старта приложения в момент, когда все
механизмы гарантированно работоспособны. Для этого
можно воспользоваться слушателем AppContext.Listener.
В данном разделе мы рассмотрим, как

LIVE DEMO

динамически зарегистрировать для сущности entity listener
на старте приложения. Возьмем следующую задачу: в
проекте имеется сущность Employee (сотрудник
компании), которая связана один-к-одному с
платформенной сущностью User (пользователь системы):

Если атрибут name сущности User изменяется, например
через стандартный экран управления пользователями,
необходимо, чтобы изменялся также и атрибут name
связанной сущности Employee. Это обычная задача для

связанной сущности Employee. Это обычная задача для
"денормализованных" данных, и решается она, как
правило, с использованием entity listeners. В данном случае
ситуация осложняется тем, что необходимо отслеживать
изменения не проектной, а платформенной сущности
User, и добавить entity listener с помощью аннотации
@Listeners невозможно. Однако, можно добавить listener
динамически через бин EntityListenerManager, и сделать
это лучше всего на старте приложения.
AppLifecycle.java - бин среднего слоя, реализующий
интерфейс AppContext.Listener, содержащий методы
applicationStarted() и applicationStopped().
UserEntityListener.java - entity listener для сущности User.
В результате сразу после старта блока Middleware будет
вызван метод applicationStarted() данного бина. В этом
методе в качестве entity listener сущности User
регистрируется бин sample_UserEntityListener.
Метод onBeforeUpdate() класса UserEntityListener
будет вызываться перед каждым сохранением изменений
экземпляров User в базу данных. В методе проверяется,
есть ли атрибут name среди измененных, и если да,
загружается связанный экземпляр Employee, и в нем
устанавливается это же значение name.

4.2. Моделирование предметной области
В данном разделе приведены рецепты, связанные с
дизайном модели данных и работой с атрибутами
сущностей.

4.2.1. Присвоение начальных значений
Присвоение начальных значений атрибутам новых
экземпляров сущностей можно производить несколькими
способами.

способами.

4.2.1.1. Инициализация полей
сущности
Атрибуты простых типов (Boolean, Integer и

LIVE DEMO

т.д.) и перечисления можно инициализировать прямо в
объявлении соответствующего поля класса сущности. См.
например поля active и grade в Customer.java.
Кроме того, в классе сущности можно создать
специальный метод инициализации и добавить ему
аннотацию @PostConstruct. В этом случае в процессе
инициализации можно использовать вызов любых
глобальных интерфейсов инфраструктуры и бинов. См.
метод init() в Customer.java.

4.2.1.2. Инициализация с помощью
CreateAction
LIVE DEMO

Если начальное значение атрибута зависит от
данных вызывающего экрана, то можно воспользоваться
методами setInitialValues() или
setInitialValuesSupplier() класса CreateAction.
См. пример работы с сущностями Customer и
CustomerAddress в демо-приложении:

customer-address-browse.xml - дескриптор экрана с двумя
связанными таблицами, одна для заказчиков, другая
для их адресов.

для их адресов.
CustomerAddressBrowse.java - контроллер экрана. В его
методе init() вызывается
setInitialValuesSupplier(), который используется
для предоставления начального значения атрибуту
customer создаваемого адреса. Значением будет
заказчик, выбранный в данный момент в первой
таблице.

4.2.1.3. Использование метода
initNewItem
Начальные значения можно также задать в

LIVE DEMO

контроллере экрана создаваемой сущности в методе
initNewItem().
Рассмотрим сущности:

В демо-приложении атрибут info сущности
CustomerDetails редактируется в том же экране, что и
сам Customer. Данный подход требует создания
экземпляра CustomerDetails вместе с владеющим им
Customer.
customer-edit.xml - дескриптор экрана редактирования
заказчика. Он содержит вложенный источник данных
для связанного экземпляра CustomerDetails.
Компонент` infoField` типа TextArea подключен к этому
источнику.
CustomerEdit.java - контроллер экрана. В нем определен
метод initNewItem(), который создает новый

экземпляр CustomerDetails и устанавливает его в
новый Customer. Созданный экземпляр будет доступен
через вложенный источник данных и сохранен в базе
данных когда экран будет закоммичен.

4.2.2. Редактирование композитных сущностей
Платформа CUBA поддерживает два типа связи между
сущностями: ассоциацию и композицию. В интерфейсе
CUBA Studio они названы соответственно ASSOCIATION и
COMPOSITION. Ассоциация - это связь между объектами,
которые могут существовать отдельно друг от друга.
Композиция же используется для связи типа "master-detail"
когда экземпляры detail существуют только в составе
master. Примером композиции может служить связь
аэропорта и терминалов: терминал, не относящийся ни к
какому аэропорту, не имеет смысла.
Как правило, редактирование сущностей, входящих в
состав композиции, удобно осуществлять совместно. То
есть, например, пользователь открывает экран
редактирования аэропорта, видит в нем список
терминалов, может создавать и редактировать их, но все
изменения, как аэропорта, так и терминалов, сохраняются
в базу данных вместе в одной транзакции, и только тогда,
когда пользователь подтвердит сохранение главной
сущности - аэропорта.

4.2.2.1. One-to-Many: один уровень
вложенности
Рассмотрим реализацию композиции на
примере сущностей Airport и Terminal:

LIVE DEMO

Terminal.java - сущность Terminal содержит
обязательную ссылку на Airport.
В редакторе сущностей Studio установите следующие
свойства для аттрибута airport: Attribute type ASSOCIATION, Cardinality - MANY_TO_ONE, Mandatory - on.
Airport.java - сущность Airport содержит one-to-many
коллекцию терминалов. Соответствующее поле
аннотировано @Composition для реализации композиции,
и @OnDelete для каскадного мягкого удаления.
В редакторе сущностей Studio установите следующие
свойства для аттрибута terminals: Attribute type COMPOSITION, Cardinality - ONE_TO_MANY, On delete - CASCADE.
views.xml - представление airport-terminals экрана
редактирования аэропорта содержит атрибутколлекцию terminals. Для этого атрибута используется
представление _local, так как атрибут airport
сущности Terminal устанавливается только во время
создания экземпляра Terminal и никогда не изменяется
после этого, поэтому загружать его не требуется.
airport-edit.xml - XML-дескриптор экрана редактирования
аэропорта определяет источник данных для
экземпляра аэропорта, и вложенный источник для его
терминалов. Кроме того, экран содержит таблицу,
отображающую терминалы.
terminal-edit.xml - стандартный редактор для сущности
Terminal.
В результате редактирование экземпляра аэропорта
работает следующим образом:
В экране редактирования аэропорта отображается
таблица терминалов.

таблица терминалов.
Пользователь может выбрать терминал и открыть экран
его редактирования. При нажатии OK в экране
редактирования терминала измененный экземпляр
терминала сохраняется не в базу данных, а в источник
данных terminalsDs экрана редактирования аэропорта.
Пользователь может создавать новые или удалять
терминалы - все изменения сохраняются в источнике
данных terminalsDs.
Пользователь нажимает OK в экране редактирования
аэропорта, и измененный Airport вместе со всеми
измененными экземплярами Terminal отправляется на
middleware в метод DataManager.commit() и сохраняется в
базе данных в рамках одной транзакции.

4.2.2.2. One-to-Many: два уровня
вложенности
Композиция может быть более глубокой и

LIVE DEMO

состоять из двух уровней вложенности. Усложним
приведенный выше пример, добавив сущность
MeetingPoint, описывающую место встречи у терминала
аэропорта:

Теперь сущность Terminal содержит атрибут
meetingPoints - коллекцию экземпляров MeetingPoint.
Для того, чтобы все три сущности представляли собой
единую композицию и редактировались совместно, нужно
в дополнение к описанному в предыдущем разделе

в дополнение к описанному в предыдущем разделе
выполнить следующее:
Terminal.java - атрибут meetingPoints класса Terminal
содержит аннотации @Composition и @OnDelete
аналогично атрибуту terminals класса Airport.
views.xml - представление terminal-meetingPoints-view
сущности Terminal содержит атрибут-коллекцию
meetingPoints. Данное представление используется в
представлении airport-terminals-meetingPointsview сущности Airport.
airport-edit.xml - дескриптор экрана редактирования
Airport содержит источники данных для экземпляра
Airport и вложенных сущностей на всю глубину
композиции (airportDs > terminalsDs >
meetingPointsDs).
Источник данных meetingPointsDs здесь не связан ни с
какими визуальными компонентами, однако он
необходим для корректной работы совместного
редактирования композиции.
terminal-edit.xml - XML-дескриптор экрана редактирования
терминала в свою очередь определяет вложенный
источник данных и соответствующую таблицу для
коллекции meetingPoints.
В результате измененные эземпляры MeetingPoint, так
же как и экземпляры Terminal, будут сохраняться в базу
данных только вместе с экземпляром Airport в одной
транзакции.

4.2.2.3. One-to-Many: три уровня
вложенности
Представьте, что вам необходима еще одна сущность,
содержащая некоторые детали места встречи

содержащая некоторые детали места встречи
(MeetingPoint). Назовем эту сущность Note. Таким образом,
вся структура будет выглядеть следующим образом:
Airport > Terminal > Meeting Point > Note.

CUBA может обеспечить работу с композициями с
максимум 2-мя уровнями вложенности. Теперь у
структура с 3-мя уровня, поэтому необходимо ограничить
глубину композиции либо сверху, либо снизу. В данном
разделе мы рассмотрим два различных (с точки зрения
user experience) подхода для исключения из композиции
аэропорта. Оба подхода решают одну и ту же проблему:
так как теперь терминалы сохраняются в базу данных
независимо от аэропорта, невозможно сохранить
терминал для только-что созданного аэропорта пока он
не сохранен в БД.
При первом подходе браузер и редактор

LIVE DEMO

аэропорта выглядят так-же как и раньше, но редактор
имеет дополнительную кнопку Save для сохранения
нового аэропорта не закрывая экрана. Пользователь не
может создавать терминалы, пока новый аэропорт не
сохранен.
airport-edit.xml содержит standalone источник данных для
терминалов вместо вложенного. Этот источник связан с
источником аэропорта, и поэтому загружает терминалы
только для редактируемого аэропорта. Кроме того,
экран содержит фрейм extendedEditWindowActions,
позволяющий пользователю сохранить аэропорт не
закрывая экран.
AirportEdit.java - здесь, в методе postInit() редактора
аэропорта, мы управляем состоянием enabled действия

аэропорта, мы управляем состоянием enabled действия
создания терминала и передаем текущий экземпляр
аэропорта для инициализации ссылки в создаваемом
терминале.
При втором подходе мы разбиваем браузер

LIVE DEMO

аэропортов на две панели: одна для списка аэропортов,
вторая для зависимого списка терминалов. Т.е. список
терминалов теперь находится вне редактора
аэропорта. Действие создания терминалов недоступно,
если не выбран ни один аэропорт.
airport-browse.xml содержит standalone источник данных
для списка терминалов. Он связан с источником
аэропортов, и загружает терминалы только для
выбранного аэропорта.
AirportBrowse.java - здесь, в методе init() браузера
аэропорта, мы управляем состоянием enabled действия
создания терминала и передаем выбранный экземпляр
аэропорта для инициализации ссылки в создаваемом
терминале.

4.2.2.4. Композиция One-to-One
Композиция one-to-one рассматривается на

LIVE DEMO

примере сущностей Customer и CustomerDetails:

Customer.java - сущность Customer содержит
необязательную ссылку на CustomerDetails,
аннотированную как @Composition.
CustomerDetails.java - сущность CustomerDetails.
customer-edit.xml - дескриптор экрана редактирования

заказчика. Он содержит вложенный источник данных
для экземпляра CustomerDetails. Для того, чтобы
загрузить вложенный экземляр, корневой источник
данных использует представление сущности Customer,
включающее атрибут details. Компонент FieldGroup
просто декларирует поле для атрибута details.
В результате редактирование экземпляра Customer
работает следующим образом:
Экран редактирования Customer содержит компонент
PickerField с двумя действиями: OpenAction и ClearAction:

Когда вызывается OpenAction, создается новый
экземпляр CustomerDetails и он отображается в
собственном экране редактирования. При нажатии OK в
этом экране, экземпляр CustomerDetails сохраняется
не в БД, а в источнике данных detailsDs редактора
Customer.
Компонент выбора отображает instance name сущности
CustomerDetails:

Когда пользователь нажимает OK в редакторе Customer,
измененный экземпляр Customer вместе с экземпляром
CustomerDetails отправляется в метод
DataManager.commit() на средний слой и сохраняется в
БД в одной транзакции.
Если пользователь вызывает ClearAction в поле
выбора, экземпляр CustomerDetails удаляется и ссылка
на него очищается в одной транзакции после коммита
редактора Customer.

4.3. Загрузка и сохранение данных

4.3. Загрузка и сохранение данных
В данном разделе рассматриваются различные способы
загрузки и сохранения данных в БД.

4.3.1. DataManager vs. EntityManager
И DataManager и EntityManager предназначены для
выполнения операций с сущностями (CRUD). Ниже
приведены различия между этими интерфейсами.
DataManager

EntityManager

DataManager доступен и на
среднем слое и на

EntityManager доступен только на среднем слое.

клиентском уровне.
DataManager является
синглтон-бином.

Ссылку на EntityManager необходимо получать
через интерфейс Persistence.

DataManager содержит

EntityManager в большой степени повторяет

несколько

стандартный

высокоуровневых методов
для работы с detached

javax.persistence.EntityManager.

сущностями: load(),

loadList(),
reload(), commit().
DataManager на самом деле делегирует выполнение
реализациям DataStore, поэтому особенности DataManager,
перечисленные ниже, актуальны только для наиболее
часто встречающегося случая, когда вы работаете с
сущностями, хранящимися в реляционной базе данных.
DataManager

EntityManager

DataManager всегда стартует новую

Для работы с EntityManager необходима

транзакцию внутри.

открытая транзакция.

DataManager загружает частичные
сущности в соответствие с

EntityManager всегда загружает все
локальные атрибуты. Если

представлением. Есть некоторые

используется представление, оно

исключения, см. подробности.

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

DataManager выполняет только JPQL

EntityManager может выполнять любые

DataManager выполняет только JPQL

EntityManager может выполнять любые

DataManager
запросы. Кроме того, он имеет

EntityManager
JPQL или native (SQL) запросы.

отдельные методы для загрузки
сущностей: load(), loadList();
и скалярных и агрегатных значений:

loadValues().
DataManager проверяет права доступа,
когда вызывается с клиентского

EntityManager не проверяет права
доступа.

уровня.

При работе на клиентском уровне доступен только
DataManager. На среднем слое, используйте EntityManager
когда необходимо реализовать атомарную логику внутри
транзакции или если его интерфейс лучше подходит для
решения задачи. В противном случае, на среднем слое
можно использовать любой из интерфейсов на выбор.
Если вам нужно обойти ограничения DataManager при
работе на клиентском уровне, создайте свой сервис и
используйте EntityManager для работы с данными. В
сервисе можно проверять права пользователя с помощью
интерфейса Security и возвращать клиенту данные в виде
персистентных или неперсистентных сущностей или
произвольных значений.

4.4. Использование REST API
Данный раздел содержит ряд примеров использования
REST API.
Детальная информация о методах REST API описана
согласно спецификации Swagger и доступна по адресу
http:// les.cuba-platform.com/swagger/6.7.

4.4.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 (значения по умолчанию для этих
свойств: client и secret). Заголовок Authorization запроса
на получение токена должен содеражть логин и пароль
клиента, разделенные символом ":" и закодированные в
base64.
Тип запроса на получение токена должен быть
application/x-www-form-urlencoded, кодировка UTF-8.
Запрос должен содержать следующие параметры:
grant_type - всегда значение password.
username - логин пользователя приложения.
password - пароль пользователя приложения.
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=smith&password=qwerty123

Метод возвращает JSON объект:
{
"access_token": "29bc6b45-83cd-4050-8c7a2a8a60adf251",
"token_type": "bearer",
"expires_in": 43198,

"expires_in": 43198,
"scope": "rest-api"
}

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

4.4.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 (значения по умолчанию для этих
свойств: client и secret). Заголовок Authorization
запроса на получение токена должен содержать логин и
пароль клиента, разделенные символом ":" и
закодированные в base64.
Запрос должен содержать следующие параметры
(соответствуют параметрам стандартной
аутентификации):
grant_type - всегда значение password.
username - логин пользователя приложения.
password - пароль пользователя приложения.
Тип запроса на получение токена должен быть
application/x-www-form-urlencoded, кодировка UTF-8.
Стандартная аутентификация может быть отключена при
помощи свойства
cuba.rest.standardAuthenticationEnabled:
cuba.rest.standardAuthenticationEnabled = false

4.4.3. Собственный механизм аутентификации
Различные механизмы аутентификации могут

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

Создайте сущность Coupon и добавьте ей атрибут code:
@Column(name = "CODE", unique = true, length = 4)
protected String code;

2.

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

3.

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




4.

Ссылку на этот файл укажите в свойстве приложения
cuba.restSpringContextConfig в файле
modules/web/src/web-app.properties:
cuba.restSpringContextConfig =
+com/company/demo/rest-dispatcher-spring.xml

5.

Создайте пакет rest в корневом каталоге модуля web, а
в нём - свой контроллер Spring MVC. В контроллере
используйте бин OAuthTokenIssuer, который позволяет
сгенерировать и выдать REST API токен после
аутентификации:
@RestController
@RequestMapping("auth-code")
public class AuthCodeController {
@Inject
private OAuthTokenIssuer oAuthTokenIssuer;
@Inject
private LoginService loginService;
@Inject
private Configuration configuration;

private Configuration configuration;
@Inject
private DataManager dataManager;
@Inject
private MessageTools messageTools;

// 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 =
loginService.getSystemSession(webAuthConfig.getTrust
edClientPassword());
} catch (LoginException e) {
throw new RuntimeException("Error
during system auth");
}
// set security context
AppContext.setSecurityContext(new
SecurityContext(systemSession));
try {
// find coupon with code
LoadContext 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, "nocache");
return new ResponseEntity<>
(accessToken, headers, HttpStatus.OK);
} finally {
// clean up security context
AppContext.setSecurityContext(null);
}
}

// POJO for JSON error messages
public static class ErrorInfo {
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;
}
}
}

6.

Исключите пакет rest из сканирования в модулях
web/core: это необходимо, так как бин OAuthTokenIssuer
доступен только внутри контекста REST API, и
сканирование его в контексте приложения будет
вызывать ошибку.




7.

Теперь пользователи могут получать код доступа

7.

Теперь пользователи могут получать код доступа
OAuth2 через обычный запрос GET HTTP, передавая
значение промо-кода в параметре code:
http://localhost:8080/app/rest/auth-code?code=A325

Результат:
{"access_token":"74202587-6c2b-4d74-bcf20d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}

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

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

Создайте пакет restapi в корневом каталоге модуля
web и поместите в него собственный контроллер Spring
MVC. Контроллер должен содержать два основных
метода: get(), возвращающий ResponseEntity, и
login(), в котором мы будем получать OAuth-токен.
FacebookAuthenticationController.java
@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:
FacebookAuthenticationController.java
@RequestMapping(method = RequestMethod.POST, value =
"login")
public ResponseEntity
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, "nostore");
headers.set(HttpHeaders.PRAGMA, "no-cache");

headers.set(HttpHeaders.PRAGMA, "no-cache");
return new ResponseEntity<>
(tokenResult.getAccessToken(), headers,
HttpStatus.OK);
}

2.

Исключите пакет restapi из сканирования в модулях
web/core: это необходимо, так как бин OAuthTokenIssuer
доступен только внутри контекста REST API, и
сканирование его в контексте приложения будет
вызывать ошибку.




3.

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


Facebook login demo with REST-API




Facebook login demo with REST-API

Login with Facebook
You are logged in!

Users

В этом скрипте мы попробуем залогиниться через Facebook. Сначала удаляем лишний код из URL, затем передаём код в REST API для получения OAuth-токена, и в случае успешной аутентификации мы сможем загружать и сохранять данные как обычно: facebook-login-demo.html var oauth2Token = null; function tryToLoginWithFacebook() { var urlHash = window.location.hash; if (urlHash && urlHash.indexOf('&code=') >= 0) { console.log("Try to login to CUBA RESTAPI!"); var urlCode = 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-wwwform-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-wwwform-urlencoded' }, success: function (data) { $('#fbLink').hide(); $('#users').show(); $.each(data, function (i, user) { $('#usersList').append("
  • " + user.name + " (" + user.email + ")
  • "); }); } }); } tryToLoginWithFacebook(); Другой пример использования JavaScript-кода в приложениях CUBA вы можете найти в разделе Пример использования из JavaScript. 4.4.4. Получение списка экземпляров сущности Предполжим, в системе имеется сущность sales$Order, и необходимо получить список экземляров этой сущности. При этом, необходимо получить не все записи, а 50 записей, начиная с сотой (для отображения третьей странице в каком-либо списке клиентского приложения). Кроме простых атрибутов сущности sales$Order результат должен содержать данные о клиенте (поле customer). Заказы должны быть отсортированы по дате. Базовый URL для получения списка экземпляров сущности sales$Order: http://localhost:8080/app/rest/v2/entities/sales$Order Для выполнения описанных выше условий необходимо задать параметры запроса: view - представление, с которым должны быть загружены сущности. В нашем примере представление order-edit-view содержит ссылку на customer. order-edit-view содержит ссылку на 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-editview&limit=50&offset=100&sort=date Ответ будет выглядеть следующим образом: [ { "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "number": "00001", "items": [ { "_entityName": "sales$OrderItem", "_instanceName": "Beach umbrella", "id": "95a04f46-af7a-a307-de4e-f2d73cfc74f7", "price": 23, "name": "Beach umbrella" }, { "_entityName": "sales$OrderItem", "_instanceName": "Sun lotion", "id": "a2129675-d158-9e3a-5496-41bf1a315917", "price": 9.9, "name": "Sun lotion" } ], "customer": { "_entityName": "sales$Customer", "_instanceName": "Toby Burns", "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05", "firstName": "Toby", "lastName": "Burns" } }, { "_entityName": "sales$Order", "_instanceName": "00002", "id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8", "date": "2016-12-31", "description": "New Year party set", "number": "00002", "items": [ { "_entityName": "sales$OrderItem", "_instanceName": "Jack Daniels", "id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe", "price": 50.7, "name": "Jack Daniels" }, { "_entityName": "sales$OrderItem", "_instanceName": "Hennessy X.O", "id": "c01be87b-3f91-7a86-50b5-30f2f0a49127", "price": 79.9, "name": "Hennessy X.O" } ], "customer": { "_entityName": "sales$Customer", "_instanceName": "Morgan Collins", "_instanceName": "Morgan Collins", "id": "5d111245-2ed0-abec-3bee-1a196da92e3e", "firstName": "Morgan", "lastName": "Collins" } } ] Обратите внимание, что для каждой сущности загружаются атрибуты _entityName с именем сущности и _instanceName, содержащий результат вычисления короткого имени для сущности. 4.4.5. Создание экземпляра сущности Для создания нового экземпляра сущности sales$Order необходимо выполнить POST запрос по адресу: http://localhost:8080/app/rest/v2/entities/sales$Order OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer. Тело запроса должно содержать JSON объект, описывающий новый экземпляр, например: { "number": "00017", "date": "2016-09-01", "description": "Back to school", "items": [ { "_entityName": "sales$OrderItem", "price": 100, "name": "School bag" }, { "_entityName": "sales$OrderItem", "_entityName": "sales$OrderItem", "price": 9.90, "name": "Pencils" } ], "customer": { "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05" } } В теле запроса передается коллекция позиций заказа items и ссылка на клиента 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.OnDelete; import com.haulmont.cuba.core.global.DeletePolicy; import javax.persistence.*; import java.util.Date; import java.util.Set; @NamePattern("%s|number") @Table(name = "SALES_ORDER") @Entity(name = "sales$Order") public class Order extends StandardEntity { private static final long serialVersionUID = 7565070704618724997L; 7565070704618724997L; @Column(name = "NUMBER_") protected String number; @Temporal(TemporalType.DATE) @Column(name = "DATE_") protected Date date; @Column(name = "DESCRIPTION") protected String description; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer; @Composition @OnDelete(DeletePolicy.CASCADE) @OneToMany(mappedBy = "order") protected Set items; //getters and setters omitted } В сущности Order коллекция items аннотирована @Composition. Методы создания и обновления сущности REST API создают новые экземпляры для всех элементов таких коллекций. Т.е. вместе с заказом (Order) будет создано две позиции заказа (OrderItem). Ссылка customer не имеет аннотации @Composition, поэтому метод REST API попытается найти клиента с переданным идентификатором и проставить его в поле customer. Если клиент не будет найден, заказ не будет создан и метод вернет ошибку. В случае успеха возвращается полный граф созданной сущности: сущности: { "_entityName": "sales$Order", "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50", "date": "2016-09-01", "description": "Back to school", "version": 1, "number": "00017", "createdBy": "admin", "createTs": "2016-10-13 18:12:21.047", "updateTs": "2016-10-13 18:12:21.047", "items": [ { "_entityName": "sales$OrderItem", "id": "3158b8ed-7b7a-568e-aec5-0822c3ebbc24", "createdBy": "admin", "price": 9.9, "name": "Pencils", "createTs": "2016-10-13 18:12:21.047", "version": 1, "updateTs": "2016-10-13 18:12:21.047", "order": { "_entityName": "sales$Order", "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50" } }, { "_entityName": "sales$OrderItem", "id": "72774b8b-4fea-6403-7b52-4a6a749215fc", "createdBy": "admin", "price": 100, "name": "School bag", "createTs": "2016-10-13 18:12:21.047", "version": 1, "updateTs": "2016-10-13 18:12:21.047", "order": { "order": { "_entityName": "sales$Order", "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50" } } ], "customer": { "_entityName": "sales$Customer", "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05", "firstName": "Toby", "lastName": "Burns", "createdBy": "admin", "createTs": "2016-10-13 15:32:01.657", "version": 1, "updateTs": "2016-10-13 15:32:01.657" } } 4.4.6. Изменение существующего экземпляра сущности Для изменения экземпляра сущности sales$Order необходимо выполнить PUT запрос по адресу: http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94d6ba-155c5c4f2a50 Последняя часть запроса здесь - это идентификатор изменяемой сущности. OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer. В теле запроса необходимо передать JSON объект, содержащий только поля, которые мы хотим изменить, например: { { "date": "2017-10-01", "customer" : { "id" : "5d111245-2ed0-abec-3bee-1a196da92e3e" } } В теле ответа будет возвращена измененная сущность: { "_entityName": "sales$Order", "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50", "date": "2017-10-01", "updatedBy": "admin", "description": "Back to school", "version": 2, "number": "00017", "createdBy": "admin", "createTs": "2016-10-13 18:12:21.047", "updateTs": "2016-10-13 19:13:02.656", "customer": { "_entityName": "sales$Customer", "id": "5d111245-2ed0-abec-3bee-1a196da92e3e", "firstName": "Morgan", "lastName": "Collins", "createdBy": "admin", "createTs": "2016-10-13 15:31:27.821", "version": 1, "updateTs": "2016-10-13 15:31:27.821", "email": "collins@gmail.com" } } 4.4.7. Выполнение JPQL-запроса (GET) Перед выполнением запроса с помощью REST API Перед выполнением запроса с помощью REST API необходимо описать его в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл restqueries.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties): cuba.rest.queriesConfig = +com/company/sales/restqueries.xml Содержимое файла rest-queries.xml: = :startDate and o.date <= :endDate]]> Для выполнения 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 - параметры запроса со значениями. 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", "items": [ { "_entityName": "sales$OrderItem", "_instanceName": "Jack Daniels", "id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe", "price": 50.7, "name": "Jack Daniels" }, { "_entityName": "sales$OrderItem", "_instanceName": "Hennessy X.O", "id": "c01be87b-3f91-7a86-50b5-30f2f0a49127", "price": 79.9, "name": "Hennessy X.O" } ], "customer": { "customer": { "_entityName": "sales$Customer", "_instanceName": "Morgan Collins", "id": "5d111245-2ed0-abec-3bee-1a196da92e3e", "firstName": "Morgan", "lastName": "Collins" } } ] Список других возможных параметров для метода выполнения запросов можно посмотреть в Swagger документации. 4.4.8. Выполнение JPQL-запроса (POST) JPQL-запрос также может быть выполнен с помощью POSTзапроса. Это необходимо для случая, когда параметр JPQL-запроса является коллекцией. В файле конфигурации JPQL-запросов для REST API тип параметраколлекции должен заканчиваться символами []: java.lang.String[], java.util.UUID[] и т.п. Параметры JPQL-запроса должны быть переданы в теле HTTP-запроса в JSON map: { "ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1f316-a7c8-7a96-87ba06afbbbd"], "status": "ready" } URL POST-запроса: http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds? returnCount=true 4.4.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$OrderItem oi where oi.order.number = :orderNumber") .setParameter("orderNumber", orderNumber) .getSingleResult(); tx.commit(); } return orderPrice; } } Перед выполнением метода сервиса с помощью REST API необходимо разрешить его вызов в конфигурационном файле. В корневом пакете модуля web (например, com.company.sales) необходимо создать файл restservices.xml. Затем этот файл объявляется в файле свойств приложения модуля web (web-app.properties): cuba.rest.servicesConfig = +com/company/sales/restservices.xml Содержимое файла rest-services.xml: Для вызова метода сервиса, необходимо выполнить 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 4.4.10. Вызов метода сервиса (POST) REST API позволяет выполнять методы сервисов, 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; 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)): Метод 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" } OAuth-токен должен быть передан в заголовке запроса Authorization с типом Bearer. Метод вернет сериализованный POJO: { "success": false, "errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01" } 4.4.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 4.4.12. Загрузка файлов Для загрузки файлов вам нужно получить OAuth-токен, который понадобится в дальнейших запросах. Используем простую форму для загрузки:

    Select a file:


    Result:

    Далее используем jQuery, чтобы получить JSON с объектом data, который, по сути, представляет собой новый FileDescriptor. Использовать загруженный файл мы 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(); } }); }); 4.4.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: 3.1.1.min.js">

    Sales

    Содержимое файла 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-formurlencoded' }, 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$Cust omer?view=_local', headers: { headers: { 'Authorization': 'Bearer ' + oauthToken, 'Content-Type': 'application/x-www-formurlencoded' }, success: function (data) { $('#customers').show(); $.each(data, function (i, customer) { $('#customersList').append("
  • " + customer.name + " (" + customer.email + ")
  • "); }); } }); } Имя пользователя и пароль из полей ввода передаётся на сервер POST-запросом с закодированными Base64 клиентскими именем и паролем в заголовке Authorization, как описано в разделе Получение OAuth токена. В случае успешной аутентификации, страница получает с сервера значение токена доступа, токен записывается в переменную oauthToken, скрывается блок loginForm и отображается блок loggedInStatus. Чтобы отобразить список покупателей, на сервер отправляется запрос на получение списка экземпляров сущности sales$Customer, передавая значение oauthToken в заголовке Authorization. Если запрос выполнен, блок customers отображается на экране, и маркированный список customersList заполняется элементами, содержащими значения атрибутов name и email сущности Customer. 4.4.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 "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.en tity.RoleType Если из URL убрать часть с именем сущности или перечисления, то будут возвращены локализованные сообщения для всех сущностей или перечислений. 4.4.15. Примеры версионирования модели данных Атрибут сущности переименован Предположим, атрибут oldNumber сущности sales$Order был переименован в newNumber, а атрибут date был переименован в deliveryDate. В этом случае конфигурация трансформации будет выглядеть следующим образом: ... Если клиентскому приложению необходимо работать со старой версии сущности sales$Order, то приложение должно передать значение версии модели данных в параметре URL modelVersion: http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4a7c0-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 выглядит так: ... В дополнение к конфигу из предыдущего примера здесь появился атрибут oldEntityName. Он описывает имя сущности, действительное для версии модели данных 1.1. Атрибут currentEntityName описывает текущее имя 1.1. Атрибут currentEntityName описывает текущее имя сущности. Хотя сущность с именем sales$Order более не существует, следующий запрос будет работать: http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4a7c0-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", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" } Атрибут должен быть удален из JSON Иногда может возникнуть ситуация, когда в сущность был добавлен новый атрибут, но клиент, работающий со старой версией модели данных, не должен получать этот атрибут при запросе к сущности. В этом случае стандартный JSON трансформер может удалить атрибут из результата. Конфиг трансформации для данного случае выглядит примерно так: ... Описание трансформации здесь содержит тег toVersion с вложенной командой removeAttribute. Это значит, что при выполнении трасформации из текущей версии модели данных к определенной версии (например, при запросе списка сущностей) атрибут discount будет удален из JSON. Если выполнить запрос к REST API без атрибута modelVersion, то атрибут discount будет возвращен. http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4a7c0-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-4ef4a7c0-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$OldOrder была переименована в sales$NewOrder. В сущности имеется поле orderDate с типом дата. В старой версии сущности это поле содержало часть со временем, в новой версии сущности часть со временем в поле отсутствует. Клиент REST API, запрашивающий сущность со старой версией модели данных 1.0 ожидает, что дата будет иметь часть со временем. Получается, что JSON трансформер должен изменить значение в JSON. Так будет выглядеть конфигурация трансформации для данного случая: ... Конфигурация содержит элемент custom с вложенными элементами toVersion и fromVersion. Эти элементы элементами toVersion и fromVersion. Эти элементы содержат ссылки на бины, т.е. кастомный трансформер должен быть зарегистрирован как Spring bean. Важная деталь: в кастомном трансформере возможно потребуется использовать бин платформы RestTransformations (он предоставляет доступ к транформерам для других сущностей). Но бин RestTransformations зарегистрирован в Spring контексте сервлета REST API, а не в главном контексте веб-приложения. Это значит, что бин кастомного трансформера также должен быть зарегистрирован в контексте REST API. Как это сделать. Во-первых, создайте файл restdispatcher-spring.xml в модуле web или portal (например в пакете com.company.test). Затем зарегистрируйте этот файл в файле свойств app.properties соответствующего модуля. cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml Файл rest-dispatcher-spring.xml должен содержать определения бинов для кастомных трансформеров: beans-4.3.xsd"> Исходный код бина 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.AbstractEntityJsonTran sformer; import com.haulmont.restapi.transform.JsonTransformationDire ction; public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer { public RepairJsonTransformerToVersion() { 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 содержит функциональность стандартных трансформеров. 4.4.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). Пример 2 Следующий пример показывает две вещи: как выполнять поиск с помощью POST запроса и как использовать группы ИЛИ. В случае POST все параметры должны быть переданы в JSON объекте в теле запроса. Условия поиска должны быть помещены в поле объекта с именем filter. Остальные параметры (имя view, количество выгружаемых сущностей и т.п.) должы быть попомещны в поля объекта с соответствующими именами: { "filter": { "conditions": [ { "group": "OR", "conditions": [ { "property": "author.lastName", "operator": "contains", "value": "Stev" }, { "property": "author.lastName", "operator": "=", "value": "Dumas" "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 также передан в теле запроса. 4.5. Разное В данном разделе собраны рецепты, которые сложно отнести к одной из категорий выше. 4.5.1. Получение локализованных сообщений В данном разделе рассмотрены способы получения локализованных сообщений в различных компонентах приложения. В XML-дескрипторах экранов атрибуты компонентов, отображающие статичный текст (например caption), могут обращаться к локализованным сообщениям по правилам метода MessageTools.loadString(). Например: правилам метода MessageTools.loadString(). Например: caption="msg://roleName" - получить сообщение, заданное ключом roleName в пакете сообщений текущего экрана. Пакет сообщений экрана задается в атрибуте messagesPack корневого элемента window. caption="msg://com.company.sample.entity/Role.nam e" - получить сообщение, заданное ключом Role.name в пакете сообщений com.company.sample.entity. В контроллерах экранов локализованные сообщения можно получать следующими способами: Из пакета сообщений текущего экрана: Методом getMessage(), унаследованным от базового класса AbstractFrame. Например: String msg = getMessage("warningMessage"); Методом formatMessage(), унаследованным от базового класса AbstractFrame. В этом случае сообщение используется для форматирования переданных параметров по правилам метода String.format(). Например: messages.properties: warningMessage = Invalid email address: '%s' Java-контроллер: String msg = formatMessage("warningMessage", email); Из произвольного пакета сообщений путем инжекции интерфейса инфраструктуры Messages. Например: интерфейса инфраструктуры Messages. Например: @Inject private Messages messages; @Override public void init(Map params) { String msg = messages.getMessage(getClass(), "warningMessage"); ... } В компонентах, управляемых контейнером Spring (управляемых бинах, сервисах, JMX-бинах, контроллерах Spring MVC модуля portal) локализованные сообщения можно получать путем инжекции интерфейса инфраструктуры Messages: @Inject protected Messages messages; ... String msg = messages.getMessage(getClass(), "warningMessage"); В любом коде приложения, где невозможна инжекция, интерфейс Messages может быть получен с помощью статического метода get() класса AppBeans: protected Messages messages = AppBeans.get(Messages.class); ... String msg = messages.getMessage(getClass(), "warningMessage"); 4.5.2. Загрузка и вывод изображений Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников: Сотрудник представлен сущностью Employee. Файлы изображений хранятся в FileStorage. Сущность Employee содержит ссылку на соответствующий FileDescriptor. Экран редактирования Employee отображает фотографию, а также дает возможность загрузить, выгрузить и очистить изображение. Класс сущности со ссылкой на файл изображения: @Table(name = "SAMPLE_EMPLOYEE") @Entity(name = "sample$Employee") public class Employee extends StandardEntity { ... @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "IMAGE_FILE_ID") protected FileDescriptor imageFile; public void setImageFile(FileDescriptor imageFile) { this.imageFile = imageFile; } public FileDescriptor getImageFile() { return imageFile; } } Представление для загрузки Employee вместе с FileDescriptor должно содержать все локальные атрибуты FileDescriptor: атрибуты FileDescriptor: ... Фрагмент XML-дескриптора экрана редактирования Employee: