Платформа CUBA. Руководство по разработке приложений Manual
User Manual: Pdf
Open the PDF directly: View PDF .
Page Count: 855 [warning: Documents this large are best viewed by clicking the View PDF Link!]
- Платформа CUBA. Руководство по разработке приложений
- Предисловие
- 1. Введение
- 2. Установка и настройка инструментария
- 3. Быстрый старт
- 4. Сборник рецептов
- 4.1. Организация бизнес-логики
- 4.2. Моделирование предметной области
- 4.3. Загрузка и сохранение данных
- 4.4. Использование REST API
- 4.4.1. Получение OAuth токена
- 4.4.2. Аутентификация LDAP в REST API
- 4.4.3. Собственный механизм аутентификации
- 4.4.4. Получение списка экземпляров сущности
- 4.4.5. Создание экземпляра сущности
- 4.4.6. Изменение существующего экземпляра сущности
- 4.4.7. Выполнение JPQL-запроса (GET)
- 4.4.8. Выполнение JPQL-запроса (POST)
- 4.4.9. Вызов метода сервиса (GET)
- 4.4.10. Вызов метода сервиса (POST)
- 4.4.11. Скачивание файлов
- 4.4.12. Загрузка файлов
- 4.4.13. Пример использования из JavaScript
- 4.4.14. Получение локализованных сообщений
- 4.4.15. Примеры версионирования модели данных
- 4.4.16. Поиск сущностей с фильтром
- 4.5. Разное
- 5. Устройство платформы
- 5.1. Архитектура
- 5.2. Общие компоненты
- 5.2.1. Модель данных
- 5.2.2. Metadata Framework
- 5.2.3. Представления
- 5.2.4. Управляемые бины
- 5.2.5. JMX-бины
- 5.2.6. Интерфейсы инфраструктуры
- 5.2.7. PersistenceHelper
- 5.2.8. AppContext
- 5.2.9. Свойства приложения
- 5.2.10. Локализация сообщений
- 5.2.11. Аутентификация пользователей
- 5.2.12. Обработка исключений
- 5.2.13. Bean Validation
- 5.2.14. Контроль доступа к атрибутам сущностей
- 5.3. Компоненты работы с базой данных
- 5.4. Компоненты среднего слоя
- 5.5. Универсальный пользовательский интерфейс
Платформа CUBA. 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.cuba-
platform.ru/manual. Обучающие видео-материалы и
презентации располагаются по адресу www.cuba-
презентации располагаются по адресу www.cuba-
platform.ru/tutorials. Онлайн демо-приложения могут быть
запущены со страницы www.cuba-platform.ru/online-demo.
Если у Вас имеются предложения по улучшению данного
руководства, обратитесь пожалуйста в службу
поддержки по адресу www.cuba-platform.ru/support. При
обнаружении ошибки в документации укажите,
пожалуйста, номер главы и приведите небольшой участок
окружающего текста для облегчения поиска.
1. 1. Введение
В данной главе приводятся сведения о назначении и
возможностях платформы CUBA.
1.1. 1.1. Обзор платформы
Платформа CUBA предназначена для разработки
корпоративных решений, характеризующихся сложной
моделью данных, десятками или сотнями экранов,
большим количеством бизнес-логики, а также
требованиями к контролю прав доступа,
масштабируемости и отказоустойчивости.
Широкий набор готовой функциональности, развитые
средства генерации кода, визуальный дизайнер
интерфейсов, а также поддержка hot deploy радикально
сокращают время и стоимость разработки решения.
Платформа полностью построена на стеке открытых Java
технологий, что позволяет использовать наработки
крупнейшей экосистемы свободного ПО в мире, а также
контролировать исходный код всего стека.
Открытая архитектура позволяет переопределять
поведение большинства механизмов платформы,
поведение большинства механизмов платформы,
обеспечивая высокую гибкость. Разработчики могут
использовать популярные Java IDE и имеют полный доступ
к исходному коду.
Приложения на платформе легко встраиваются в ИТ-
инфраструктуру благодаря поддержке основных баз
данных и серверов приложений, а также возможности
работы в облаке. Платформа позволяет обеспечить
отказоустойчивость и масштабируемость решений и
предоставляет средства интеграции с внешними
системами.
1.2. 1.2. Технические требования
Минимальные требования для ведения разработки на
платформе CUBA:
Оперативная память - 4 ГБ.
Место на жестком диске - 5 ГБ.
Операционная система - Microsoft WindowsMicrosoft Windows, LinuxLinux или
macOSmacOS.
1.3. Release Notes1.3. Release Notes
Список изменений в платформе доступен по адресу
http://les.cuba-platform.com/cuba/release-notes/6.7
2. 2. Установка и настройка
инструментария
Минимально необходимым набором программного
обеспечения является:
Java SE Development Kit (JDK) 8Java 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.
Для WindowsWindows это можно сделать, открыв Компьютер →
Свойства системы → Дополнительные параметры
системы → Дополнительно → Переменные среды, и
задав значение переменной в списке Системные
переменные.
Для macOSmacOS JAVA_HOME рекомендуется задать в
~/.bash_profile:
export JAVA_HOME=$(/usr/libexec/java_home -v
1.8)
Если вы установили Java 9 на macOS, CUBA Studio и
приложения перестанут запускаться, независимо от
значения JAVA_HOME. В этом случае обратитесь к
информации в разделе решение проблем ниже.
CCреда разработки на Java Java
IntelliJ IDEAIntelliJ IDEA или EclipseEclipse. Рекомендуется использовать IntelliJIntelliJ
IDEAIDEA (Community или Ultimate).
База данных
В простейшем случае в качестве сервера баз данных
приложений используется встроенный HyperSQLHyperSQL
(http://hsqldb.org), что вполне подходит для исследования
возможностей платформы и прототипирования
приложений. Для создания реальных приложений
рекомендуется установить и использовать в проекте
какую-либо из полноценных СУБД, поддерживаемых
платформой, например PostgreSQLPostgreSQL.
Веб--браузер
Веб-интерфейс приложений, создаваемых на основе
платформы, поддерживает все популярные
современные браузеры, в том числе Google ChromeGoogle Chrome, MozillaMozilla
FirefoxFirefox, SafariSafari, Opera 15+Opera 15+, Internet Explorer 9+Internet Explorer 9+, Microsoft EdgeMicrosoft Edge.
Решение проблем
1. Если по какой-то причине вы установили Java 9 на
macOS, CUBA Studio и приложения перестанут
запускаться. Для того, чтобы восстановить
работоспособность, выполните следующее:
a. Сделайте инсталляцию JDK 9 не используемой по
умолчанию в системе: переименуйте файл
/Library/Java/JavaVirtualMachines/jdk-
9.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 TomcatApache Tomcat, который
используется во время разработки. Перезагрузите
компьютер после удаления переменных.
2.1. 2.1. Установка CUBA Studio 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 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 ServerCUBA Studio Server введите следующие
параметры:
Server portServer port − порт, на котором будет запущен сервер
CUBA Studio (по умолчанию 8111).
Remote connectionRemote connection - по умолчанию Studio принимает
соединения только с локального компьютера.
Установите данный флажок, если вам нужна
возможность подключения к этому экземпляру Studio
с удаленного хоста.
Silent startupSilent startup - если выбрано, сервер Studio запускается
в трее и открывает UI в браузере автоматически.
Данная опция доступна только в Windows.
5. Запустите сервер Studio, нажав кнопку StartStart.
Когда запустится веб-сервер, в поле URLURL отобразится
адрес, по которому доступен интерфейс Studio. Нажав
→, можно открыть веб-браузер, нажав CopyCopy −
скопировать адрес в буфер обмена.
6. Запустите веб-браузер и перейдите по указанному
адресу. В веб-интерфейсе Studio перейдите на вкладку
SettingsSettings и введите следующие параметры:
Java homeJava home − JDK, который будет использоваться для
сборки и запуска проектов. Если вы установили
переменную окружения JAVA_HOME как описано в
начале данной главы, ее значение будет
подставлено в данное поле. В противном случае
Studio попытается самостоятельно найти каталог
Studio попытается самостоятельно найти каталог
установки Java.
Gradle homeGradle home - путь к Gradle. Оставьте поле пустым, в
этом случае при первом запуске будет
автоматически загружен нужный дистрибутив
Gradle.
Если по какой-либо причине Вы хотите использовать
уже установленный на компьютере Gradle, введите в
поле путь к соответствующему каталогу. Текущая
версия системы сборки проектов протестирована на
Gradle 3.4Gradle 3.4.
IDE portIDE port − порт, на котором принимает подключения
плагин IDE (по умолчанию 48561).
OineOine - включить возможность работы без интернет-
соединения при условии, что все необходимые
библиотеки были предварительно загружены из
репозитория.
Check for updatesCheck for updates - проверять наличие новых версий
при старте.
Send anonymous statistics and crash reportsSend anonymous statistics and crash reports - разрешить
Studio отправлять статистику ошибок
разработчикам.
Help languageHelp language - язык встроенной справки.
Logging levelLogging level - уровень логирования: TRACE, DEBUG, INFO,
WARN, ERROR или FATAL. По умолчанию INFO.
7. Нажмите Apply and proceed to projectsApply and proceed to projects.
8. Нажмите Create newCreate new для создания нового проекта, или
ImportImport для добавления имеющегося проекта в список
RecentRecent.
9. Сразу после открытия проекта Studio загружает
исходный код компонентов платформы, на которых
основан проект, и сохраняет его в локальном
каталоге. Перед сборкой приложения рекомендуется
дождаться окончания загрузки и убедиться в том, что
индикатор фоновых задач в левом нижнем углу
экрана Studio погас.
Обновление CUBA 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. 2.2. Интеграция CUBA Studio CUBA Studio с IDE IDE
2.2. 2.2. Интеграция CUBA Studio CUBA Studio с IDE IDE
Для интеграции с IntelliJ IDEAIntelliJ IDEA или EclipseEclipse выполните
следующие шаги:
1. Откройте или создайте новый проект в Studio.
2. Перейдите в секцию Project propertiesProject properties и нажмите кнопку
EditEdit. Выберите нужную Java IDEJava IDE флажками IntelliJ IDEAIntelliJ IDEA или
EclipseEclipse.
3. В главном меню Studio выберите пункт меню Build Build →
Create or update <IDE> project lesCreate or update <IDE> project les. В каталоге проекта будут
созданы соответствующие файлы.
4. Для интеграции с IntelliJ IDEA:
a. Запустите IntelliJ IDEA 13+ и установите плагин CUBACUBA
Framework IntegrationFramework Integration, доступный в репозитории
плагинов: File > Settings > Plugins > Browse RepositoriesFile > Settings > Plugins > Browse Repositories.
5. Для интеграции с Eclipse:
a. Запустите Eclipse 4.3+, откройте Help > Install NewHelp > Install New
SoftwareSoftware, добавьте репозиторий http://files.cuba-
platform.com/eclipse-update-site и установите
плагин CUBA PluginCUBA Plugin.
b. В Eclipse в меню Window > PreferencesWindow > Preferences в секции CUBACUBA
установите флажок Studio Integration EnabledStudio Integration Enabled и нажмите
на кнопку OKOK.
Обратите внимание, что в панели статуса Studio
загорелась надпись IDE: on port 48561IDE: on port 48561. Теперь при нажатии
кнопок IDEIDE в Studio соответствующие файлы исходных
кодов будут открываться редактором IDE.
3. 3. Быстрый старт
В данном разделе рассматривается создание приложения
при помощи CUBA Studio. Эта же информация изложена в
видеороликах, доступных по адресу www.cuba-
видеороликах, доступных по адресу www.cuba-
platform.ru/quickstart.
На Вашей рабочей машине уже должно быть установлено
и настроено необходимое программное обеспечение, см.
Установка и настройка инструментария.
Основные задачи, стоящие при разработке нашего
приложения:
1. Разработка модели данных, которая заключается в
создании сущностей предметной области и
соответствующих таблиц базы данных.
2. Разработка экранов пользовательского интерфейса,
позволяющих создавать, просматривать, обновлять и
удалять сущности модели данных.
3.1. 3.1. Описание задачи
Приложение предназначено для ведения сведений о
покупателях и их заказах.
Покупатель имеет следующие характеристики:
Имя
Электронная почта
Характеристики заказа:
Принадлежность покупателю
Дата
Сумма
Пользовательский интерфейс приложения должен
содержать:
содержать:
Окно списка покупателей;
Окно редактирования сведений о покупателе,
содержащее также список заказов данного покупателя;
Окно общего списка заказов;
Окно редактирования заказа.
3.2. 3.2. Создание проекта
1. Запустите CUBA StudioCUBA Studio и откройте ее веб-интерфейс (см.
Установка CUBA Studio).
2. Нажмите на кнопку Create newCreate new.
3. В окне New projectNew project в поле Project nameProject name введите имя
проекта - sales. Имя должно содержать только
латинские буквы, цифры и знак подчеркивания.
Тщательно продумайте имя проекта на данном этапе,
так как в дальнейшем изменить его будет достаточно
сложно.
4. В полях ниже автоматически сгенерируются:
Project pathProject path − путь к каталогу нового проекта. Каталог
можно выбрать вручную, нажав на кнопку … рядом с
полем. Отобразится окно Folder selectFolder select со списком папок
на жестком диске. Вы можете выбрать одну из них или
создать новый каталог, нажав на кнопку ++.
Project namespaceProject namespace - пространство имен, которое будет
использоваться как префикс имен сущностей и
таблиц базы данных. Пространство имен может
состоять только из латинских букв, и должно быть как
можно короче. Например, если имя проекта - sales_2,
то пространство имен может быть sales или sal.
Root packageRoot package − корневой пакет Java-классов. Может
быть скорректирован позже, однако сгенерированные
на этапе создания классы перемещены не будут.
RepositoryRepository − URL и параметры аутентификации
RepositoryRepository − URL и параметры аутентификации
репозитория бинарных артефактов.
Platform versionPlatform version - используемая в проекте версия
платформы. Артефакты платформы будут
автоматически загружены из репозитория при сборке
проекта.
5. Нажмите на кнопку OKOK. В указанном каталоге sales
будет создан пустой проект, и откроется главное окно
Studio.
6. Сборка проекта. Выберите пункт главного меню Studio
BuildBuild → Assemble projectAssemble project. На этом этапе будут загружены
все необходимые библиотеки, и в подкаталогах build
модулей будут собраны артефакты проекта.
7. Создание базы данных на локальном сервере HyperSQLHyperSQL.
Выберите пункт меню RunRun → Create databaseCreate database. Имя БД по
умолчанию совпадает с пространством имен проекта.
8. Выберите пункт меню RunRun → DeployDeploy. В подкаталоге
deploy проекта будет установлен сервер TomcatTomcat с
собранным приложением.
9. Выберите пункт меню RunRun → Start application serverStart application server. Через
несколько секунд в панели статуса ссылка рядом с
надписью Web applicationWeb application станет доступной, и по ней
можно осуществить переход к приложению
непосредственно из Studio.
непосредственно из Studio.
Логин и пароль пользователя − admin / admin.
Запущенное приложение содержит два главных пункта
меню (AdministrationAdministration и HelpHelp), функциональность
подсистемы безопасности и администрирования
системы.
3.3. 3.3. Создание сущностей
Создадим класс сущности Customer (Покупатель).
Перейдите на вкладку Data ModelData Model на панели навигатора
и нажмите на кнопку NewNew > EntityEntity. Появится диалоговое
окно New entityNew entity.
В поле Class nameClass name введите название класса сущности −
Customer.
Нажмите OKOK. В рабочей области откроется страница
дизайнера сущности.
В полях NameName и TableTable автоматически сгенерируются имя
сущности и имя таблицы в базе данных.
В поле Parent classParent class оставьте установленное значение −
StandardEntity.
Поле Inheritance strategyInheritance strategy оставьте пустым.
Далее создадим атрибуты сущности. Для этого нажмите
на кнопку NewNew, находящуюся под таблицей AttributesAttributes.
В отобразившемся окне Create attributeCreate attribute в поле NameName
введите название атрибута сущности − name, в списке
Attribute typeAttribute type выберите значение DATATYPE, в поле TypeType
укажите тип атрибута String и далее укажите длину
текстового атрибута в поле LengthLength, равной 100 символам.
Установите флажок MandatoryMandatory. В поле ColumnColumn
автоматически сгенерируется имя колонки таблицы в
базе данных.
Для добавления атрибута нажмите на кнопку AddAdd.
Атрибут email создается таким же образом, за
исключением того, что в поле LengthLength следует указать
значение 50.
После создания атрибутов перейдите на вкладку InstanceInstance
NameName дизайнера сущности для задания Name pattern. В
списке Available attributesAvailable attributes выделите атрибут name и
перенесите его в список Name pattern attributesName pattern attributes, нажав на
кнопку с изображением стрелки вправо.
На этом создание сущности Customer завершено. Нажмите
на кнопку OKOK в верхней панели для сохранения
изменений.
Создадим сущность Order (Заказ). В панели DATA MODELDATA MODEL
нажмите на кнопку NewNew > EntityEntity. В поле Class nameClass name введите
название класса сущности − Order. Сущность должна
иметь следующие атрибуты:
NameName − customer, Attribute typeAttribute type − ASSOCIATION, TypeType −
Customer, CardinalityCardinality − MANY_TO_ONE.
NameName − date, Attribute typeAttribute type − DATATYPE, TypeType − Date. Для
атрибута date установите флажок MandatoryMandatory.
NameName − amount, Attribute typeAttribute type − DATATYPE, TypeType − BigDecimal.
3.4. 3.4. Создание таблиц базы данных
Для создания таблиц базы данных достаточно на вкладке
Data ModelData Model панели навигатора нажать на кнопку Generate DBGenerate DB
scriptsscripts. После этого откроется страница Database ScriptsDatabase Scripts. На
вкладке будут сгенерированы скрипты обновления базы
данных от ее текущего состояния (UPDATE SCRIPTSUPDATE SCRIPTS) и
скрипты создания базы данных с нуля (INIT TABLESINIT TABLES, INITINIT
TABLESTABLES, INIT DATAINIT DATA). Также на вкладке будут доступны уже
выполненные скрипты обновления базы данных, если они
есть.
Чтобы сохранить сгенерированные скрипты, нажмите на
кнопку Save and closeSave and close. Для запуска скриптов обновления
остановите запущенное приложение с помощью команды
RunRun → Stop application serverStop application server, затем выполните RunRun → UpdateUpdate
databasedatabase.
3.5. 3.5. Создание экранов пользовательского
интерфейса
Создадим экраны приложения, позволяющие управлять
информацией о покупателях и заказах.
3.5.1. 3.5.1. Экраны управления Покупателями
Для создания стандартных экранов просмотра и
редактирования покупателей необходимо выделить
сущность Customer на вкладке Data ModelData Model панели
навигатора и нажать на кнопку NewNew > Generic UI screenGeneric UI screen внизу
панели. После этого на экране отобразится страница
создания стандартных экранов сущности.
В списке доступных шаблонов выберите Entity browser andEntity browser and
editor screenseditor screens.
Все поля этого окна заполнены значениями по умолчанию,
пока не будем их менять. Нажмите на кнопку CreateCreate и
затем CloseClose.
Во вкладке Generic UIGeneric UI панели навигатора в модуле WebWeb
ModuleModule появятся элементы customer-browse.xml и
customer-edit.xml.
3.5.2. 3.5.2. Экраны управления Заказами
Сущность Order (Заказ) имеет следующую особенность:
так как среди прочих атрибутов существует ссылочный
атрибут Order.customer, требуется определить
представление сущности Order, включающее этот
атрибут (стандартное представление _local не включает
ссылочных атрибутов).
Для этого перейдите на вкладку Data ModelData Model на панели
навигатора, выделите сущность Order и выберите NewNew >
ViewView. Отобразится страница дизайнера представлений. В
качестве имени введите order-with-customer, в списке
атрибутов нажмите на атрибут customer и на
отобразившейся справа панели выберите представление
_minimal для сущности Customer.
Нажмите на кнопку OKOK в верхней панели.
Далее выделите сущность Order и выберите NewNew > GenericGeneric
UI screenUI screen.
В отобразившемся окне выберите представление order-
with-customer в полях ViewView для браузера и редактора и
нажмите на кнопку CreateCreate и, затем, CloseClose.
Во вкладке Generic UIGeneric UI панели навигатора в модуле WebWeb
ModuleModule появятся элементы order-edit.xml и order-
browse.xml.
3.5.3. 3.5.3. Меню приложения
3.5.3. 3.5.3. Меню приложения
При создании экраны были добавлены в пункт меню
applicationapplication, имеющийся по умолчанию. Переименуем его.
Для этого перейдите на вкладку Generic UIGeneric UI на панели
навигатора и нажмите на ссылку Open web menuOpen web menu.
Отобразится страница дизайнера меню. Выделите пункт
меню application для просмотра его свойств.
В поле IdId введите новое значение идентификатора меню −
shop. После редактирования меню нажмите на кнопку OKOK в
верхней панели.
3.5.4. 3.5.4. Экран редактирования Покупателя со
списком Заказов
Займемся задачей отображения списка заказов в окне
редактирования покупателя.
Перейдите на вкладку Generic UIGeneric UI на панели навигатора.
Выделите экран customer-edit.xml и нажмите на
кнопку EditEdit.
На странице дизайнера экрана перейдите на вкладку
DatasourcesDatasources и нажмите на кнопку NewNew.
DatasourcesDatasources и нажмите на кнопку NewNew.
Выделите только что созданный источник данных в
списке. В правой части страницы отобразятся его
характеристики.
В поле TypeType укажите collectionDatasource.
В списке EntityEntity выберите сущность Order.
В поле IdId будет автоматически заполнено значение
идентификатора источника данных − ordersDs.
В списке ViewView выберите представление _local.
В поле QueryQuery введите следующий запрос:
Здесь запрос содержит условие отбора Заказов с
параметром ds$customerDs. Значением параметра с
именем вида ds${datasource_name} будет
идентификатор сущности, установленной в данный
момент в источнике данных datasource_name, в данном
случае − идентификатор редактируемого Покупателя.
select e from sales$Order e where e.customer.id =
:ds$customerDs order by e.date
Нажмите на кнопку ApplyApply для сохранения изменений.
Далее перейдите на вкладку LayoutLayout в дизайнере экрана
и в палитре компонентов найдите компонент Label.
Перетащите этот компонент на панель иерархии
компонентов экрана, между fieldGroup и
windowActions. Перейдите на вкладку PropertiesProperties на
панели свойств. В поле valuevalue введите значение
компонента: Orders.
Если разрабатываемое приложение предполагает
локализацию на несколько языков, используйте
кнопку рядом с полем valuevalue, чтобы создать новое
сообщение msg://orders и задать его значение на
требуемых языках.
Перетащите компонент Table из палитры компонентов
на панель иерархии компонентов между label и
windowActions. Выделите компонент в иерархии и
перейдите на вкладку PropertiesProperties. Задайте размеры
таблицы: в поле widthwidth укажите 100%, в поле heightheight
установите значение 200px. Из списка доступных
источников данных выберите orderDs, после этого в
поле idid с помощью кнопки сгенерируйте
идентификатор таблицы: ordersTable.
Для сохранения изменений в экране редактирования
Покупателя нажмите на кнопку OKOK в верхней панели.
3.6. 3.6. Запуск приложения
Посмотрим, как созданные нами экраны выглядят в
работающем приложении. Для этого выполните RunRun > StartStart
application serverapplication server.
Зайдите в систему, использовав стандартные имя и
пароль в окне логина. Откройте пункт меню ShopShop >
CustomersCustomers:
Рисунок 1. 1. Экран списка Customers Customers
Нажмите на кнопку CreateCreate и создайте нового покупателя:
Рисунок 2. 2. Экран редактирования Customer Customer
Откройте пункт меню ShopShop > OrdersOrders:
Рисунок 3. 3. Экран списка Orders Orders
Нажмите на кнопку CreateCreate и создайте новый заказ, выбрав
в поле CustomerCustomer только что созданного покупателя:
Рисунок 4. 4. Экран редактирования Order Order
В таблице на экране редактирования покупателя теперь
отображается только что созданный заказ:
Рисунок 5. 5. Экран редактирования Customer Customer
4. 4. Сборник рецептов
Данный раздел представляет собой коллекцию
практических рецептов по решению типичных задач,
встающих перед разработчиками при использовании
платформы CUBA. В каждом подразделе информация
организована от простого к сложному, поэтому вы в любой
LIVE DEMOLIVE DEMO
момент можете перейти к другой секции или заняться
кодированием.
Большинство разделов снабжены демо-приложениями.
Вы можете увидеть их в работе онлайн, посмотреть
исходный код на GitHub, или загрузить и запустить
локально. Ссылки на проекты приложений доступны
также на вкладке SamplesSamples в Studio.
Данный раздел находится в работе и будет
постоянно дополняться.
4.1. 4.1. Организация бизнес--логики
Один из первых вопросов, возникающих в начале
процесса разработки на платформе, это "где мне
расположить мою бизнес-логику"? Использование Studio
сильно облегчает создание модели данных и CRUD
экранов, любой реальный проект требует создания логики
помимо загрузки и сохранения данных. Данный раздел
содержит информацию о том, как эффективно
организовать бизнес-логику в зависимости от задачи.
Большинство примеров в данном разделе работают со
следующей моделью:
В этих примерах мы будем рассчитывать скидки для
заказчиков в зависимости от общей суммы их покупок.
4.1.1. 4.1.1. Бизнес--логика в контроллерах
Например, необходимо запускать расчет
LIVE DEMOLIVE DEMO
скидки, когда пользователь нажимает кнопку на экране-
браузере заказчиков. В этом случае, наиболее простое
решение - это разместить логику расчета прямо в
контроллере экрана.
См. кнопку Calculate discountCalculate discount в демо-приложении и
реализацию контроллера: CustomerBrowse.java. Пожалуйста,
имейте в виду, что данная имплементацию расчета не
является оптимальной (см. варианты работы с данными в
разделе Загрузка и сохранение данных).
Данный подход приемлем, если логика вызывается из
одного места и она не слишком сложна, чтобы уместиться
в нескольких коротких методах.
4.1.2. 4.1.2. Использование бинов клиентского уровня
Теперь давайте немного усложним задачу из
предыдущего раздела. Допустим, требуется вызывать
расчет из двух экранов: и из браузера, и из редактора.
Чтобы не дублировать код, нужно извлечь код из
контроллера и поместить в некоторое общедоступное
место. Это может быть управляемый бин клиентского
уровня.
Управляемый бин - это класс с аннотацией @Component. Он
может быть инжектирован в другие бины и контроллеры
экранов, или получен с помощью статического метода
AppBeans.get(). Если класс бина реализует некоторый
интерфейс, то к нему можно обращаться через этот
интерфейс.
Имейте в виду, что для того чтобы бин был доступен для
контроллеров экранов, он должен располагаться в одном
из следующих модулей: globalglobal, guigui или webweb вашего проекта.
В случае globalglobal, бин будет также доступен на среднем
слое.
См. кнопку Calculate discountCalculate discount на экранах браузера и
LIVE DEMOLIVE DEMO
См. кнопку Calculate discountCalculate discount на экранах браузера и
редактора в демо-приложении, и реализацию:
CustomerBrowse.java - контроллер браузера.
CustomerEdit.java - контроллер редактора.
DiscountCalculator.java - бин расчета скидок. Он использует
DataManager для загрузки списка заказов данного
заказчика из базы данных.
4.1.3. 4.1.3. Использование сервисов среднего слоя
В предыдущем разделе мы рассмотрели
инкапсуляцию бизнес-логики в бине клиентского уровня.
Теперь мы пойдем дальше и поместим нашу логику в
наиболее подходящее место: на средний слой. Сделав это,
мы достигнем следующих целей:
Наши бизнес-методы будут доступны клиентам всех
типов, включая Polymer UI.
Мы сможем использовать API, доступный только на
middleware: EntityManager, transactions, и т.п.
Чтобы вызвать бизнес-метод среднего слоя, необходимо
создать сервис. Studio может помочь в создании заготовки
сервиса:
Переключитесь на вкладку MiddlewareMiddleware и нажмите New >New >
ServiceService.
Измените имя интерфейса сервиса на DiscountService.
Имена класса и самого сервиса будут изменены
соответственно. Нажмите OKOK или ApplyApply.
соответственно. Нажмите OKOK или ApplyApply.
Нажмите IDEIDE и откройте интерфейс сервиса в IDE.
Создайте новый метод и реализуйте его в классе
сервиса.
См. пример реализации в демо-приложении:
CustomerBrowse.java and CustomerEdit.java - контроллер
экрана, который вызывает сервис.
DiscountService.java - интерфейс сервиса.
DiscountServiceBean.java - класс реализации сервиса.
DiscountCalculator.java - бин среднего слоя,
рассчитывающий скидки. Разумеется, сервис мог бы
содержать бизнес-логику сам, но мы будем
использовать этот делегат для того чтобы разделять
логику с entity listener и JMX-бином (см. следующие
разделы).
Обратите внимание, что данный бин отличается от
рассмотренного в предыдущем разделе: он расположен
в модуле corecore и использует EntityManager для загрузки
суммы заказов из базы данных.
Теперь давайте сделаем наш бизнес-метод доступным
для внешних клиентов через REST API:
Откройте сервис на редактирование в Studio и
переключитесь на вкладку REST MethodsREST Methods.
Установите для метода флажок REST invocation allowedREST invocation allowed.
LIVE DEMOLIVE DEMO
Studio создаст файл rest-services.xml и зарегистрирует
в нем метод. После перезапуска сервера вы сможете
вызвать метод с помощью HTTP-запросов. Например,
следующий GET-запрос должен работать с нашим онлайн
демо-сервером:
https://demo1.cuba-platform.com/business-
logic/rest/v2/services/sample_DiscountService/calculateDiscount?
customerId=1797f54d-5bec-87a6-4330-d958955743a2
Имейте в виду, что демо-приложение имеет анонимный
доступ. В большинстве реальных сценариев
использования необходимо будет аутентифицироваться,
прежде чем выполнять запросы.
4.1.4. 4.1.4. Использование Entity Listeners Entity Listeners
Entity listeners позволяют выполнять бизнес-
логику каждый раз, когда сущность создается,
изменяется или удаляется в базе данных. Например, мы
можем пересчитывать скидку для заказчика каждый раз,
когда некоторый заказ для него изменяется.
Заготовку для entity listener можно легко создать в Studio:
Перейдите на вкладку MiddlewareMiddleware и нажмите New > EntityNew > Entity
listenerlistener.
Измените имя класса на OrderEntityListener и
включите флажки для интерфейсов
BeforeInsertEntityListener,
BeforeUpdateEntityListener и
BeforeDeleteEntityListener.
Выберите сущность Order в поле Entity typeEntity type.
Нажмите OKOK или ApplyApply и откройте класс listener в IDE.
См. пример в демо-приложении:
LIVE DEMOLIVE DEMO
OrderEntityListener.java - класс entity listener.
DiscountCalculator.java - бин среднего слоя,
рассчитывающий скидки. Entity listener мог бы содержать
бизнес-логику сам, но мы использовуем этот делегат
для того, чтобы разделять логику с сервисом и JMX
бином.
Если вы откроете экран Logic in Entity ListenersLogic in Entity Listeners демо-
приложения, вы увидите две таблицы: заказы и заказчики.
Создайте, измените или удалите заказ, обновите таблицу
заказчиков, и вы увидите, что скидка для
соответствующего заказчика изменилась.
4.1.5. 4.1.5. Использование JMX- JMX-бинов
С помощью JMX-бинов можно предоставить
доступ к некоторой административной
функциональности вашего приложения без создания
пользовательского интерфейса для нее. Данная
функциональность будет также доступна через
встроенную JMX-консоль и через внешние инструменты
JMX, например jconsole.
В нашем примере со скидками пользователь, имеющий
доступ к JMX-консоли, сможет пересчитывать скидки для
всех заказчиков или для заказчика с указанным id.
Studio на данный момент не умеет создавать заготовки
JMX-бинов, поэтому все классы и конфигурационные
элементы придется создавать вручную в IDE.
См. пример реализации в демо-приложении:
LIVE DEMOLIVE DEMO
DiscountsMBean.java - интерфейс JMX-бина.
Discounts.java - реализация JMX-бина.
DiscountCalculator.java - бин среднего слоя, вызываемый
JMX-бином. JMX-бин мог бы содержать бизнес-логику
сам, но мы использовуем этот делегат для того, чтобы
разделять логику с entity listener и JMX бином.
spring.xml - в данном файле JMX-бин регистрируется.
4.1.6. 4.1.6. Запуск кода на старте приложения
Иногда бывает необходимо выполнить некоторый код
сразу после старта приложения в момент, когда все
механизмы гарантированно работоспособны. Для этого
можно воспользоваться слушателем AppContext.Listener.
В данном разделе мы рассмотрим, как
динамически зарегистрировать для сущности 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. Моделирование предметной области
В данном разделе приведены рецепты, связанные с
дизайном модели данных и работой с атрибутами
сущностей.
4.2.1. 4.2.1. Присвоение начальных значений
Присвоение начальных значений атрибутам новых
экземпляров сущностей можно производить несколькими
способами.
LIVE DEMOLIVE DEMO
LIVE DEMOLIVE DEMO
способами.
4.2.1.1. 4.2.1.1. Инициализация полей
сущности
Атрибуты простых типов (Boolean, Integer и
т.д.) и перечисления можно инициализировать прямо в
объявлении соответствующего поля класса сущности. См.
например поля active и grade в Customer.java.
Кроме того, в классе сущности можно создать
специальный метод инициализации и добавить ему
аннотацию @PostConstruct. В этом случае в процессе
инициализации можно использовать вызов любых
глобальных интерфейсов инфраструктуры и бинов. См.
метод init() в Customer.java.
4.2.1.2. 4.2.1.2. Инициализация с помощью
CreateActionCreateAction
Если начальное значение атрибута зависит от
данных вызывающего экрана, то можно воспользоваться
методами setInitialValues() или
setInitialValuesSupplier() класса CreateAction.
См. пример работы с сущностями Customer и
CustomerAddress в демо-приложении:
customer-address-browse.xml - дескриптор экрана с двумя
связанными таблицами, одна для заказчиков, другая
для их адресов.
LIVE DEMOLIVE DEMO
для их адресов.
CustomerAddressBrowse.java - контроллер экрана. В его
методе init() вызывается
setInitialValuesSupplier(), который используется
для предоставления начального значения атрибуту
customer создаваемого адреса. Значением будет
заказчик, выбранный в данный момент в первой
таблице.
4.2.1.3. 4.2.1.3. Использование метода
initNewIteminitNewItem
Начальные значения можно также задать в
контроллере экрана создаваемой сущности в методе
initNewItem().
Рассмотрим сущности:
В демо-приложении атрибут info сущности
CustomerDetails редактируется в том же экране, что и
сам Customer. Данный подход требует создания
экземпляра CustomerDetails вместе с владеющим им
Customer.
customer-edit.xml - дескриптор экрана редактирования
заказчика. Он содержит вложенный источник данных
для связанного экземпляра CustomerDetails.
Компонент` infoField` типа TextArea подключен к этому
источнику.
CustomerEdit.java - контроллер экрана. В нем определен
метод initNewItem(), который создает новый
LIVE DEMOLIVE DEMO
экземпляр CustomerDetails и устанавливает его в
новый Customer. Созданный экземпляр будет доступен
через вложенный источник данных и сохранен в базе
данных когда экран будет закоммичен.
4.2.2. 4.2.2. Редактирование композитных сущностей
Платформа CUBA поддерживает два типа связи между
сущностями: ассоциацию и композицию. В интерфейсе
CUBA Studio они названы соответственно ASSOCIATION и
COMPOSITION. Ассоциация - это связь между объектами,
которые могут существовать отдельно друг от друга.
Композиция же используется для связи типа "master-detail"
когда экземпляры detail существуют только в составе
master. Примером композиции может служить связь
аэропорта и терминалов: терминал, не относящийся ни к
какому аэропорту, не имеет смысла.
Как правило, редактирование сущностей, входящих в
состав композиции, удобно осуществлять совместно. То
есть, например, пользователь открывает экран
редактирования аэропорта, видит в нем список
терминалов, может создавать и редактировать их, но все
изменения, как аэропорта, так и терминалов, сохраняются
в базу данных вместе в одной транзакции, и только тогда,
когда пользователь подтвердит сохранение главной
сущности - аэропорта.
4.2.2.1. One-to-Many: 4.2.2.1. One-to-Many: один уровень
вложенности
Рассмотрим реализацию композиции на
примере сущностей Airport и Terminal:
Terminal.java - сущность Terminal содержит
обязательную ссылку на Airport.
В редакторе сущностей Studio установите следующие
свойства для аттрибута airport: Attribute typeAttribute type -
ASSOCIATION, CardinalityCardinality - MANY_TO_ONE, MandatoryMandatory - on.
Airport.java - сущность Airport содержит one-to-many
коллекцию терминалов. Соответствующее поле
аннотировано @Composition для реализации композиции,
и @OnDelete для каскадного мягкого удаления.
В редакторе сущностей Studio установите следующие
свойства для аттрибута terminals: Attribute typeAttribute type -
COMPOSITION, CardinalityCardinality - ONE_TO_MANY, On deleteOn delete - CASCADE.
views.xml - представление airport-terminals экрана
редактирования аэропорта содержит атрибут-
коллекцию terminals. Для этого атрибута используется
представление _local, так как атрибут airport
сущности Terminal устанавливается только во время
создания экземпляра Terminal и никогда не изменяется
после этого, поэтому загружать его не требуется.
airport-edit.xml - XML-дескриптор экрана редактирования
аэропорта определяет источник данных для
экземпляра аэропорта, и вложенный источник для его
терминалов. Кроме того, экран содержит таблицу,
отображающую терминалы.
terminal-edit.xml - стандартный редактор для сущности
Terminal.
В результате редактирование экземпляра аэропорта
работает следующим образом:
В экране редактирования аэропорта отображается
таблица терминалов.
LIVE DEMOLIVE DEMO
таблица терминалов.
Пользователь может выбрать терминал и открыть экран
его редактирования. При нажатии OKOK в экране
редактирования терминала измененный экземпляр
терминала сохраняется не в базу данных, а в источник
данных terminalsDs экрана редактирования аэропорта.
Пользователь может создавать новые или удалять
терминалы - все изменения сохраняются в источнике
данных terminalsDs.
Пользователь нажимает OKOK в экране редактирования
аэропорта, и измененный Airport вместе со всеми
измененными экземплярами Terminal отправляется на
middleware в метод DataManager.commit() и сохраняется в
базе данных в рамках одной транзакции.
4.2.2.2. One-to-Many: 4.2.2.2. One-to-Many: два уровня
вложенности
Композиция может быть более глубокой и
состоять из двух уровней вложенности. Усложним
приведенный выше пример, добавив сущность
MeetingPoint, описывающую место встречи у терминала
аэропорта:
Теперь сущность Terminal содержит атрибут
meetingPoints - коллекцию экземпляров MeetingPoint.
Для того, чтобы все три сущности представляли собой
единую композицию и редактировались совместно, нужно
в дополнение к описанному в предыдущем разделе
в дополнение к описанному в предыдущем разделе
выполнить следующее:
Terminal.java - атрибут meetingPoints класса Terminal
содержит аннотации @Composition и @OnDelete
аналогично атрибуту terminals класса Airport.
views.xml - представление terminal-meetingPoints-view
сущности Terminal содержит атрибут-коллекцию
meetingPoints. Данное представление используется в
представлении airport-terminals-meetingPoints-
view сущности 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: 4.2.2.3. One-to-Many: три уровня
вложенности
Представьте, что вам необходима еще одна сущность,
содержащая некоторые детали места встречи
LIVE DEMOLIVE DEMO
содержащая некоторые детали места встречи
(MeetingPoint). Назовем эту сущность NoteNote. Таким образом,
вся структура будет выглядеть следующим образом:
Airport > Terminal > Meeting Point > NoteAirport > Terminal > Meeting Point > Note.
CUBA может обеспечить работу с композициями с
максимум 2-мя уровнями вложенности. Теперь у
структура с 3-мя уровня, поэтому необходимо ограничить
глубину композиции либо сверху, либо снизу. В данном
разделе мы рассмотрим два различных (с точки зрения
user experience) подхода для исключения из композиции
аэропорта. Оба подхода решают одну и ту же проблему:
так как теперь терминалы сохраняются в базу данных
независимо от аэропорта, невозможно сохранить
терминал для только-что созданного аэропорта пока он
не сохранен в БД.
При первом подходе браузер и редактор
аэропорта выглядят так-же как и раньше, но редактор
имеет дополнительную кнопку SaveSave для сохранения
нового аэропорта не закрывая экрана. Пользователь не
может создавать терминалы, пока новый аэропорт не
сохранен.
airport-edit.xml содержит standalone источник данных для
терминалов вместо вложенного. Этот источник связан с
источником аэропорта, и поэтому загружает терминалы
только для редактируемого аэропорта. Кроме того,
экран содержит фрейм extendedEditWindowActions,
позволяющий пользователю сохранить аэропорт не
закрывая экран.
AirportEdit.java - здесь, в методе postInit() редактора
аэропорта, мы управляем состоянием enabled действия
LIVE DEMOLIVE DEMO
LIVE DEMOLIVE DEMO
аэропорта, мы управляем состоянием enabled действия
создания терминала и передаем текущий экземпляр
аэропорта для инициализации ссылки в создаваемом
терминале.
При втором подходе мы разбиваем браузер
аэропортов на две панели: одна для списка аэропортов,
вторая для зависимого списка терминалов. Т.е. список
терминалов теперь находится вне редактора
аэропорта. Действие создания терминалов недоступно,
если не выбран ни один аэропорт.
airport-browse.xml содержит standalone источник данных
для списка терминалов. Он связан с источником
аэропортов, и загружает терминалы только для
выбранного аэропорта.
AirportBrowse.java - здесь, в методе init() браузера
аэропорта, мы управляем состоянием enabled действия
создания терминала и передаем выбранный экземпляр
аэропорта для инициализации ссылки в создаваемом
терминале.
4.2.2.4. 4.2.2.4. Композиция One-to-One One-to-One
Композиция one-to-one рассматривается на
примере сущностей 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 и он отображается в
собственном экране редактирования. При нажатии OKOK в
этом экране, экземпляр CustomerDetails сохраняется
не в БД, а в источнике данных detailsDs редактора
Customer.
Компонент выбора отображает instance name сущности
CustomerDetails:
Когда пользователь нажимает OKOK в редакторе Customer,
измененный экземпляр Customer вместе с экземпляром
CustomerDetails отправляется в метод
DataManager.commit() на средний слой и сохраняется в
БД в одной транзакции.
Если пользователь вызывает ClearAction в поле
выбора, экземпляр CustomerDetails удаляется и ссылка
на него очищается в одной транзакции после коммита
редактора Customer.
4.3. 4.3. Загрузка и сохранение данных
4.3. 4.3. Загрузка и сохранение данных
В данном разделе рассматриваются различные способы
загрузки и сохранения данных в БД.
4.3.1. DataManager vs. EntityManager4.3.1. DataManager vs. EntityManager
И DataManager и EntityManager предназначены для
выполнения операций с сущностями (CRUD). Ниже
приведены различия между этими интерфейсами.
DataManagerDataManager EntityManagerEntityManager
DataManager доступен и на
среднем слое и на
клиентском уровне.
EntityManager доступен только на среднем слое.
DataManager является
синглтон-бином.
Ссылку на EntityManager необходимо получать
через интерфейс Persistence.
DataManager содержит
несколько
высокоуровневых методов
для работы с detached
сущностями: load(),
loadList(),
reload(), commit().
EntityManager в большой степени повторяет
стандартный
javax.persistence.EntityManager.
DataManager на самом деле делегирует выполнение
реализациям DataStore, поэтому особенности DataManager,
перечисленные ниже, актуальны только для наиболее
часто встречающегося случая, когда вы работаете с
сущностями, хранящимися в реляционной базе данных.
DataManagerDataManager EntityManagerEntityManager
DataManager всегда стартует новую
транзакцию внутри.
Для работы с EntityManager необходима
открытая транзакция.
DataManager загружает
частичные
сущности в соответствие с
представлением. Есть некоторые
исключения, см. подробности.
EntityManager всегда загружает все
локальные атрибуты. Если
используется представление, оно
влияет только на загрузку ссылочных
атрибутов. См. подробности.
DataManager выполняет только JPQL EntityManager может выполнять любые
DataManager выполняет только JPQL
запросы. Кроме того, он имеет
отдельные методы для загрузки
сущностей: load(), loadList();
и скалярных и агрегатных значений:
loadValues().
EntityManager может выполнять любые
JPQL или native (SQL) запросы.
DataManager проверяет права доступа,
когда вызывается с клиентского
уровня.
EntityManager не проверяет права
доступа.
DataManagerDataManager EntityManagerEntityManager
При работе на клиентском уровне доступен только
DataManager. На среднем слое, используйте EntityManager
когда необходимо реализовать атомарную логику внутри
транзакции или если его интерфейс лучше подходит для
решения задачи. В противном случае, на среднем слое
можно использовать любой из интерфейсов на выбор.
Если вам нужно обойти ограничения DataManager при
работе на клиентском уровне, создайте свой сервис и
используйте EntityManager для работы с данными. В
сервисе можно проверять права пользователя с помощью
интерфейса Security и возвращать клиенту данные в виде
персистентных или неперсистентных сущностей или
произвольных значений.
4.4. 4.4. Использование REST API REST API
Данный раздел содержит ряд примеров использования
REST API.
Детальная информация о методах REST API описана
согласно спецификации Swagger и доступна по адресу
http://les.cuba-platform.com/swagger/6.7.
4.4.1. 4.4.1. Получение OAuth 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 - пароль пользователя приложения.
Метод возвращает JSON объект:
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=smith&password=qwerty123
{
"access_token": "29bc6b45-83cd-4050-8c7a-
2a8a60adf251",
"token_type": "bearer",
"expires_in": 43198,
Значение токена содержится в поле access_token
4.4.2. 4.4.2. Аутентификация LDAP LDAP в REST API 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:
"expires_in": 43198,
"scope": "rest-api"
}
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:
4.4.3. 4.4.3. Собственный механизм аутентификации
Различные механизмы аутентификации могут
cuba.rest.standardAuthenticationEnabled = false
Различные механизмы аутентификации могут
предоставлять токен по ключу, по ссылке, по логину и
паролю LDAP и т.д. Стандартный механизм
аутентификации в REST API изменить нельзя, но можно
создать свой механизм. Для этого необходимо создать
REST-контроллер, который предоставит свой URL для входа
в приложение.
В этом примере мы рассмотрим механизм
аутентификации, позволяющий получить OAuth-токен по
промо-коду. За основу возьмём приложение, содержащее
сущность Coupon (Купон) с атрибутом code (промо-код).
Значение этого атрибута мы будем передавать в качестве
параметра аутентификации в GET-запросе.
1. Создайте сущность Coupon и добавьте ей атрибут code:
2. Создайте нового пользователя с логином promo-userpromo-user, от
лица которого будет выполняться аутентификация по
промо-коду.
3. В корневом каталоге модуля webweb (com.company.demo)
создайте новый файл конфигурации Spring rest-
dispatcher-spring.xml со следующим содержанием:
@Column(name = "CODE", unique = true, length = 4)
protected String code;
<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/s
4. Ссылку на этот файл укажите в свойстве приложения
cuba.restSpringContextConfig в файле
modules/web/src/web-app.properties:
5. Создайте пакет rest в корневом каталоге модуля webweb, а
в нём - свой контроллер Spring MVC. В контроллере
используйте бин OAuthTokenIssuer, который позволяет
сгенерировать и выдать REST API токен после
аутентификации:
xsi:schemaLocation="http://www.springframework.org/s
chema/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>
cuba.restSpringContextConfig =
+com/company/demo/rest-dispatcher-spring.xml
@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<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);
}
}
6. Исключите пакет rest из сканирования в модулях
web/coreweb/core: это необходимо, так как бин OAuthTokenIssuer
доступен только внутри контекста REST API, и
сканирование его в контексте приложения будет
вызывать ошибку.
7. Теперь пользователи могут получать код доступа
// 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;
}
}
}
<context:component-scan base-
package="com.company.demo">
<context:exclude-filter type="regex"
expression="com\.company\.demo\.web\.rest\..*"/>
</context:component-scan>
7. Теперь пользователи могут получать код доступа
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, как описано в общей документации.
4.4.3.1. Social Login 4.4.3.1. Social Login в REST API REST API
Механизм авторизации через социальные сети, или social
login, также можно использовать в REST API. Исходный код
приложения, описанного в этом примере, доступен на
GitHub, а сам пример описан в разделе Social Login. Ниже
приведены ключевые моменты этого примера,
позволяющие получить OAuth-токен через аккаунт
Facebook.
1. Создайте пакет restapi в корневом каталоге модуля
webweb и поместите в него собственный контроллер Spring
MVC. Контроллер должен содержать два основных
метода: get(), возвращающий ResponseEntity, и
login(), в котором мы будем получать OAuth-токен.
FacebookAuthenticationController.javaFacebookAuthenticationController.java
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity get() {
String loginUrl = getAsPrivilegedUser(() ->
facebookService.getLoginUrl(getAppUrl(),
OAuth2ResponseType.CODE_TOKEN)
);
Здесь мы проверяем переданный код Facebook,
получаем код доступа и издаём токен с помощью
OAuthTokenIssuer:
FacebookAuthenticationController.javaFacebookAuthenticationController.java
);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.LOCATION, loginUrl);
return new ResponseEntity<>(headers,
HttpStatus.FOUND);
}
@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");
2. Исключите пакет restapi из сканирования в модулях
web/coreweb/core: это необходимо, так как бин OAuthTokenIssuer
доступен только внутри контекста REST API, и
сканирование его в контексте приложения будет
вызывать ошибку.
3. Создайте файл facebook-login-demo.html в каталоге
проекта modules/web/web/VAADIN. Он будет содержать
JavaScript-код, выполняющийся на HTML-странице:
facebook-login-demo.htmlfacebook-login-demo.html
headers.set(HttpHeaders.PRAGMA, "no-cache");
return new ResponseEntity<>
(tokenResult.getAccessToken(), headers,
HttpStatus.OK);
}
<context:component-scan base-
package="com.company.demo">
<context:exclude-filter type="regex"
expression="com\.company\.demo\.restapi\..*"/>
</context:component-scan>
<html>
<head>
<title>Facebook login demo with REST-API</title>
<script src="jquery-3.2.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"...>
В этом скрипте мы попробуем залогиниться через
Facebook. Сначала удаляем лишний код из URL, затем
передаём код в REST API для получения OAuth-токена, и в
случае успешной аутентификации мы сможем
загружать и сохранять данные как обычно:
facebook-login-demo.htmlfacebook-login-demo.html
<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>
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 =
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();
Другой пример использования JavaScript-кода в
приложениях CUBA вы можете найти в разделе Пример
использования из JavaScript.
4.4.4. 4.4.4. Получение списка экземпляров сущности
Предполжим, в системе имеется сущность sales$Order, и
необходимо получить список экземляров этой сущности.
При этом, необходимо получить не все записи, а 50
записей, начиная с сотой (для отображения третьей
странице в каком-либо списке клиентского приложения).
Кроме простых атрибутов сущности sales$Order
результат должен содержать данные о клиенте (поле
customer). Заказы должны быть отсортированы по дате.
Базовый URL для получения списка экземпляров сущности
sales$Order:
http://localhost:8080/app/rest/v2/entities/sales$Order
Для выполнения описанных выше условий необходимо
задать параметры запроса:
viewview - представление, с которым должны быть
загружены сущности. В нашем примере представление
order-edit-view содержит ссылку на customer.
$('#users').show();
$.each(data, function (i, user) {
$('#usersList').append("<li>" +
user.name + " (" + user.email + ")</li>");
});
}
});
}
tryToLoginWithFacebook();
order-edit-view содержит ссылку на customer.
limitlimit - количество возвращаемыех экземпляров.
offsetoffset - позиция первого извлеченного элемента.
sortsort - имя атрибута сущности, по которому будет
произведена сортировка.
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
В итоге получаем следующий GETGET http-запрос:
http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit-
view&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",
Обратите внимание, что для каждой сущности
загружаются атрибуты _entityName с именем сущности и
_instanceName, содержащий результат вычисления
короткого имени для сущности.
4.4.5. 4.4.5. Создание экземпляра сущности
Для создания нового экземпляра сущности sales$Order
необходимо выполнить POSTPOST запрос по адресу:
http://localhost:8080/app/rest/v2/entities/sales$Order
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
Тело запроса должно содержать JSON объект,
описывающий новый экземпляр, например:
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
{
"number": "00017",
"date": "2016-09-01",
"description": "Back to school",
"items": [
{
"_entityName": "sales$OrderItem",
"price": 100,
"name": "School bag"
},
{
"_entityName": "sales$OrderItem",
В теле запроса передается коллекция позиций заказа
items и ссылка на клиента customer. Рассмотрим, как
будут обработаны эти атрибуты.
Саначала посмотрим на класс Order:
"_entityName": "sales$OrderItem",
"price": 9.90,
"name": "Pencils"
}
],
"customer": {
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05"
}
}
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;
В сущности Order коллекция items аннотирована
@Composition. Методы создания и обновления сущности
REST API создают новые экземпляры для всех элементов
таких коллекций. Т.е. вместе с заказом (Order) будет
создано две позиции заказа (OrderItem).
Ссылка customer не имеет аннотации @Composition,
поэтому метод REST API попытается найти клиента с
переданным идентификатором и проставить его в поле
customer. Если клиент не будет найден, заказ не будет
создан и метод вернет ошибку.
В случае успеха возвращается полный граф созданной
сущности:
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<OrderItem> items;
//getters and setters omitted
}
сущности:
{
"_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": {
4.4.6. 4.4.6. Изменение существующего экземпляра
сущности
Для изменения экземпляра сущности sales$Order
необходимо выполнить PUTPUT запрос по адресу:
http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94-
d6ba-155c5c4f2a50
Последняя часть запроса здесь - это идентификатор
изменяемой сущности.
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
В теле запроса необходимо передать JSON объект,
содержащий только поля, которые мы хотим изменить,
например:
"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.7. 4.4.7. Выполнение JPQL- JPQL-запроса (GET) (GET)
Перед выполнением запроса с помощью REST API
{
"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"
}
}
Перед выполнением запроса с помощью REST API
необходимо описать его в конфигурационном файле. В
корневом пакете модуля webweb (например,
com.company.sales) необходимо создать файл rest-
queries.xml. Затем этот файл объявляется в файле
свойств приложения модуля webweb (web-app.properties):
Содержимое файла rest-queries.xml:
Для выполнения JPQL запроса, необходимо выполнить GETGET
http-запрос к REST API:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersAfterDate?
startDate=2016-11-01&endDate=2017-11-01
Части URL:
cuba.rest.queriesConfig = +com/company/sales/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-view">
<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>
sales$Order - имя извлекаемой сущности.
ordersAfterDate - имя запроса из конфигурационного
файла.
startDate и endDate - параметры запроса со
значениями.
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
Метод возвращает 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": {
Список других возможных параметров для метода
выполнения запросов можно посмотреть в Swagger
документации.
4.4.8. 4.4.8. Выполнение JPQL- JPQL-запроса (POST) (POST)
JPQL-запрос также может быть выполнен с помощью POST-
запроса. Это необходимо для случая, когда параметр
JPQL-запроса является коллекцией. В файле
конфигурации JPQL-запросов для REST API тип параметра-
коллекции должен заканчиваться символами []:
java.lang.String[], java.util.UUID[] и т.п.
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-
queries.xsd">
<query name="ordersByIds" entity="sales$Order"
view="order-edit-view">
<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>
Параметры JPQL-запроса должны быть переданы в теле
HTTP-запроса в JSON map:
URL POST-запроса:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds?
returnCount=true
4.4.9. 4.4.9. Вызов метода сервиса (GET) (GET)
Предположим, в системе имеется сервис OrderService,
реализация которого выглядит следующим образом:
</params>
</query>
</queries>
{
"ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a",
"e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-
f316-a7c8-7a96-87ba06afbbbd"],
"status": "ready"
}
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
{
Перед выполнением метода сервиса с помощью REST API
необходимо разрешить его вызов в конфигурационном
файле. В корневом пакете модуля webweb (например,
com.company.sales) необходимо создать файл rest-
services.xml. Затем этот файл объявляется в файле
свойств приложения модуля webweb (web-app.properties):
@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;
}
}
cuba.rest.servicesConfig = +com/company/sales/rest-
services.xml
Содержимое файла rest-services.xml:
Для вызова метода сервиса, необходимо выполнить GETGET
http-запрос к REST API вида:
http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?
orderNumber=00001
Части URL:
sales_OrderService - имя сервиса
calculatePrice - имя метода сервиса
orderNumber - аргумент метода со значением
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
Метод сервиса может вернуть как простой тип данных,
так и сущность, коллекцию сущностей или произвольный
POJO. В нашем случае метод возвращает BigDecimal,
поэтому в теле ответа нам вернется число:
39.2
4.4.10. 4.4.10. Вызов метода сервиса (POST) (POST)
REST API позволяет выполнять методы сервисов,
<?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>
REST API позволяет выполнять методы сервисов,
аргументами которых являются не только простые типы,
но также:
сущности
коллекции сущностей
произвольные POJO
Небольшой пример. Предположим, в сервис OrderService,
созданный в предыдущем разделе, добавлен следующий
метод:
Класс OrderValidationResult выглядит следующим
образом:
@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;
}
package com.company.sales.service;
import java.io.Serializable;
public class OrderValidationResult implements
Serializable {
private boolean success;
private String errorMessage;
Новый метод сервиса принимает сущность Order в
качестве первого аргумента и возвращает POJO.
Перед вызовом данного метода с помощью REST API
необходимо разрешить его, добавив запись в
конфигурационный файл rest-services.xml (его
создание было рассмотрено в Вызов метода сервиса
(GET)):
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;
}
}
<?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">
Метод validateOrder сервиса вызвается POSTPOST запросом по
адресу:
http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder
Параметры в случае POST передаются в теле запроса в
JSON объекте. Каждое поле JSON объекта соответствует
аргументу метода сервиса:
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
Метод вернет сериализованный POJO:
<method name="validateOrder">
<param name="order"/>
<param name="validationDate"/>
</method>
</service>
</services>
{
"order" : {
"number": "00050",
"date" : "2016-01-01"
},
"validationDate": "2016-10-01"
}
{
"success": false,
"errorMessage": "Validation of order 00050 failed.
validationDate parameter is: 2016-10-01"
}
4.4.11. 4.4.11. Скачивание файлов
При скачивании файла передавать токен в заголовке
запроса часто оказывается неудобно. Хочется иметь URL
для скачивания, который можно подставить например в
атрибут srcsrc тега imgimg. В этом случае передача OAuth-токена
возможна в самом запросе в параметре с именем
access_tokenaccess_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. 4.4.12. Загрузка файлов
Для загрузки файлов вам нужно получить OAuth-токен,
который понадобится в дальнейших запросах.
Используем простую форму для загрузки:
Далее используем jQuery, чтобы получить JSON с объектом
data, который, по сути, представляет собой новый
FileDescriptor. Использовать загруженный файл мы
<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"/>
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. 4.4.13. Пример использования из JavaScript 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, так что
содержимое каталога должно быть примерно таким:
Содержимое файла customers.html:
bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js
<html>
<head>
<script type="text/javascript" src="jquery-
3.1.1.min.js"></script>
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>
Содержимое файла customers.js:
</body>
</html>
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$Cust
omer?view=_local',
headers: {
Имя пользователя и пароль из полей ввода передаётся на
сервер POST-запросом с закодированными Base64
клиентскими именем и паролем в заголовке
Authorization, как описано в разделе Получение OAuth
токена. В случае успешной аутентификации, страница
получает с сервера значение токена доступа, токен
записывается в переменную oauthToken, скрывается блок
loginForm и отображается блок loggedInStatus.
Чтобы отобразить список покупателей, на сервер
отправляется запрос на получение списка экземпляров
сущности sales$Customer, передавая значение
oauthToken в заголовке Authorization.
Если запрос выполнен, блок customers отображается на
экране, и маркированный список customersList
заполняется элементами, содержащими значения
атрибутов name и email сущности Customer.
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>");
});
}
});
}
4.4.14. 4.4.14. Получение локализованных сообщений
REST API позволяет получить локализованные заголовки
для сущностей, перечислений и их атрибутов.
Например, чтобы получить локализованные сообщения
для сущности sec$User, необходимо выполнить
следующий GETGET запрос:
http://localhost:8080/app/rest/v2/messages/entities/sec$User
OAuth-токен должен быть передан в заголовке запроса
Authorization с типом BearerBearer.
Явно указать локаль запроса можно с помощью http-
заголовка Accept-LanguageAccept-Language.
Ответ будет выглядеть следующим образом:
{
"sec$User": "User",
"sec$User.active": "Active",
"sec$User.changePasswordAtNextLogon": "Change
Для получения списка локализованных сообщений для
перечисления используется следующий запрос:
http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.en
tity.RoleType
Если из URL убрать часть с именем сущности или
перечисления, то будут возвращены локализованные
сообщения для всех сущностей или перечислений.
"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"
}
4.4.15. 4.4.15. Примеры версионирования модели
данных
Атрибут сущности переименован
Предположим, атрибут oldNumber сущности sales$Order
был переименован в newNumber, а атрибут date был
переименован в deliveryDate. В этом случае
конфигурация трансформации будет выглядеть
следующим образом:
Если клиентскому приложению необходимо работать со
старой версии сущности sales$Order, то приложение
должно передать значение версии модели данных в
параметре URL modelVersion:
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-
a7c0-dff348347f93?modelVersion=1.0
Будет возвращен следующий результат:
<?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>
{
"_entityName": "sales$Order",
Видим, что ответ содержит атрибуты oldNumber и date,
хотя сущность в последней версии приложения уже
имеет переименованные атрибуты newNumber и
deliveryDate.
Имя сущности изменено
Предположим, что в одном из следующих релизов
приложения имя сущности sales$Order также было
изменено. Новое имя сущности теперь sales$NewOrder.
Конфиг трансформации для версии 1.1 выглядит так:
В дополнение к конфигу из предыдущего примера здесь
появился атрибут oldEntityName. Он описывает имя
сущности, действительное для версии модели данных
1.1. Атрибут currentEntityName описывает текущее имя
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
<?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>
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:
Клиентское приложение также может использовать
старую версию модели данных для создания или
изменения сущности.
Этот POSTPOST запрос использует старое имя сущности и
имеет 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"
}
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
Атрибут должен быть удален из JSON JSON
Иногда может возникнуть ситуация, когда в сущность
был добавлен новый атрибут, но клиент, работающий со
старой версией модели данных, не должен получать
этот атрибут при запросе к сущности. В этом случае
стандартный JSON трансформер может удалить атрибут
из результата.
Конфиг трансформации для данного случае выглядит
примерно так:
Описание трансформации здесь содержит тег toVersion
с вложенной командой removeAttribute. Это значит, что
при выполнении трасформации из текущей версии
модели данных к определенной версии (например, при
запросе списка сущностей) атрибут discount будет
удален из JSON.
"date": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
<?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>
Если выполнить запрос к REST API без атрибута
modelVersion, то атрибут discount будет возвращен.
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-
a7c0-dff348347f93
Если указать modelVersion в запросе, то атрибут
discount будет удален:
http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-
a7c0-dff348347f93?modelVersion=1.1
Использование кастомных трансформеров
Вы также можете создать и зарегистрировать свой
собственный трансформер JSON. В качестве примера
рассмотрим следующую ситуацию.
Сущность sales$OldOrder была переименована в
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"deliveryDate": "2016-10-31",
"description": "Vacation order",
"number": "00001",
"discount": 50
}
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"deliveryDate": "2016-10-31",
"description": "Vacation order",
"oldNumber": "00001"
}
Сущность sales$OldOrder была переименована в
sales$NewOrder. В сущности имеется поле orderDate с
типом дата. В старой версии сущности это поле
содержало часть со временем, в новой версии сущности
часть со временем в поле отсутствует. Клиент REST API,
запрашивающий сущность со старой версией модели
данных 1.0 ожидает, что дата будет иметь часть со
временем. Получается, что JSON трансформер должен
изменить значение в JSON.
Так будет выглядеть конфигурация трансформации для
данного случая:
Конфигурация содержит элемент custom с вложенными
элементами toVersion и fromVersion. Эти элементы
<?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_OrderJsonTransformerFromVer
sion"/>
<toVersion
transformerBeanRef="sales_OrderJsonTransformerToVersi
on"/>
</custom>
</transformation>
...
</transformations>
элементами toVersion и fromVersion. Эти элементы
содержат ссылки на бины, т.е. кастомный трансформер
должен быть зарегистрирован как Spring bean.
Важная деталь: в кастомном трансформере возможно
потребуется использовать бин платформы
RestTransformations (он предоставляет доступ к
транформерам для других сущностей). Но бин
RestTransformations зарегистрирован в Spring
контексте сервлета REST API, а не в главном контексте
веб-приложения. Это значит, что бин кастомного
трансформера также должен быть зарегистрирован в
контексте REST API.
Как это сделать. Во-первых, создайте файл rest-
dispatcher-spring.xml в модуле webweb или portalportal
(например в пакете com.company.test).
Затем зарегистрируйте этот файл в файле свойств
app.properties соответствующего модуля.
Файл rest-dispatcher-spring.xml должен содержать
определения бинов для кастомных трансформеров:
cuba.restSpringContextConfig =
+com/company/test/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/sc
hema/beans
http://www.springframework.org/schema/beans/spring-
beans-4.3.xsd">
Исходный код бина
sales_OrderJsonTransformerToVersion:
beans-4.3.xsd">
<bean
name="sales_OrderJsonTransformerFromVersion"
class="com.company.test.transformer.OrderJsonTransfo
rmerFromVersion"/>
<bean name="sales_OrderJsonTransformerToVersion"
class="com.company.test.transformer.OrderJsonTransfo
rmerToVersion"/>
</beans>
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);
}
Данный трансформер находит элемент orderDate в JSON
и изменяет значение элемента, добавляя к нему часть со
временем.
Когда сущность sales$Order будет запрошена с версией
модели данных 1.0, то JSON результат будет содержать
сущность, поле orderDate которой содержит часть со
временем, хотя текущий тип поля сущности - дата без
времени.
Несколько слов о кастомных трансформерах. Они
должны реализовывать интерфейс
EntityJsonTransformer. Вы также можете
отнаследоваться от класса
AbstractEntityJsonTransformer и переопределить его
метод doCustomTransformations. Класс
AbstractEntityJsonTransformer содержит
функциональность стандартных трансформеров.
@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");
}
}
}
4.4.16. 4.4.16. Поиск сущностей с фильтром
REST API даёт возможность искать экземпляры сущностей
по определенным условиям.
Примеры мы рассмотрим на модели данных, состоящей из
двух сущностей:
AuthorAuthor с двумя полями: lastName и firstName
BookBook с тремя полями: title (String), author (Author) и
publicationYear (Integer)
Чтобы выполнить поиск с фильром URL должен выглядеть
следующим образом:
http://localhost:8080/app/rest/v2/entities/test$Book/search
Условия поиска должны быть переданы в параметре
filter. filter - это JSON объект, который содержит набор
условий поиска. Если поиск выполняется с помощью GET
запроса, то параметр filter передается в URL.
Пример 1 1
Необходимо найти все книги, опубликованные в 2007
году с именем автора, начинающимся с "Alex". JSON
фильтр для подобного условия будет выглядеть так:
{
"conditions": [
{
"property": "author.firstName",
"operator": "startsWith",
"value": "Alex"
},
{
"property": "publicationDate",
"operator": "=",
"value": 2007
}
По умолчанию все критерии поиска применяются с
условием И.
Данный пример также демонстрирует возможность
использования вложенных свойств объекта
(author.firstName).
Пример 2 2
Следующий пример показывает две вещи: как
выполнять поиск с помощью POST запроса и как
использовать группы ИЛИ. В случае POST все параметры
должны быть переданы в JSON объекте в теле запроса.
Условия поиска должны быть помещены в поле объекта
с именем filter. Остальные параметры (имя view,
количество выгружаемых сущностей и т.п.) должы быть
попомещны в поля объекта с соответствующими
именами:
}
]
}
{
"filter": {
"conditions": [
{
"group": "OR",
"conditions": [
{
"property": "author.lastName",
"operator": "contains",
"value": "Stev"
},
{
"property": "author.lastName",
"operator": "=",
"value": "Dumas"
В этом примере коллекция conditions содержит не
только объекты с критериями поиска, но и группу ИЛИ
(OR). Итоговое условие можно представить так:
((author.lastName contains Stev) OR (author.lastName = Duma) AND
(publicationDate in [2007, 2008]))
Обратите внимание, что параметр view также передан в
теле запроса.
4.5. 4.5. Разное
В данном разделе собраны рецепты, которые сложно
отнести к одной из категорий выше.
4.5.1. 4.5.1. Получение локализованных сообщений
В данном разделе рассмотрены способы получения
локализованных сообщений в различных компонентах
приложения.
В XML-дескрипторах экранов атрибуты компонентов,
отображающие статичный текст (например caption),
могут обращаться к локализованным сообщениям по
правилам метода MessageTools.loadString(). Например:
"value": "Dumas"
}
]
},
{
"property": "publicationDate",
"operator": "=",
"in": [2007, 2008]
}
]
},
"view": "book-view"
}
правилам метода 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. Например:
Методом formatMessage(), унаследованным от
базового класса AbstractFrame. В этом случае
сообщение используется для форматирования
переданных параметров по правилам метода
String.format(). Например:
messages.properties:
Java-контроллер:
Из произвольного пакета сообщений путем инжекции
интерфейса инфраструктуры Messages. Например:
String msg = getMessage("warningMessage");
warningMessage = Invalid email address: '%s'
String msg = formatMessage("warningMessage",
email);
интерфейса инфраструктуры Messages. Например:
В компонентах, управляемых контейнером Spring
(управляемых бинах, сервисах, JMX-бинах, контроллерах
Spring MVC модуля portalportal) локализованные сообщения
можно получать путем инжекции интерфейса
инфраструктуры Messages:
В любом коде приложения, где невозможна инжекция,
интерфейс Messages может быть получен с помощью
статического метода get() класса AppBeans:
@Inject
private Messages messages;
@Override
public void init(Map<String, Object> params) {
String msg = messages.getMessage(getClass(),
"warningMessage");
...
}
@Inject
protected Messages messages;
...
String msg = messages.getMessage(getClass(),
"warningMessage");
protected Messages messages =
AppBeans.get(Messages.class);
...
String msg = messages.getMessage(getClass(),
"warningMessage");
4.5.2. 4.5.2. Загрузка и вывод изображений
Рассмотрим задачу загрузки, хранения и отображения
фотографий сотрудников:
Сотрудник представлен сущностью Employee.
Файлы изображений хранятся в FileStorage. Сущность
Employee содержит ссылку на соответствующий
FileDescriptor.
Экран редактирования Employee отображает
фотографию, а также дает возможность загрузить,
выгрузить и очистить изображение.
Класс сущности со ссылкой на файл изображения:
Представление для загрузки Employee вместе с
FileDescriptor должно содержать все локальные
атрибуты FileDescriptor:
@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;
}
}
атрибуты FileDescriptor:
Фрагмент XML-дескриптора экрана редактирования
Employee:
Компоненты отображения и загрузки/выгрузки
фотографии заключены внутрь контейнера groupBox. В
верхней его части с помощью компонента image
<view class="com.company.sample.entity.Employee"
name="employee-edit">
<property name="name"/>
...
<property name="imageFile"
view="_local">
</property>
</view>
<groupBox caption="Photo" spacing="true"
height="250px" width="250px"
expand="image">
<image id="image"
width="100%"
align="MIDDLE_CENTER"
scaleMode="CONTAIN"/>
<hbox align="BOTTOM_LEFT"
spacing="true">
<upload id="uploadField"/>
<button id="downloadImageBtn"
caption="Download"
invoke="onDownloadImageBtnClick"/>
<button id="clearImageBtn"
caption="Clear"
invoke="onClearImageBtnClick"/>
</hbox>
</groupBox>
верхней его части с помощью компонента image
выводится изображение, а в нижней слева направо
расположены компонент upload для загрузки файла и
кнопки выгрузки и очистки изображения. В результате
эта часть экрана должна выглядеть следующим образом:
Теперь рассмотрим контроллер экрана редактирования.
import com.haulmont.cuba.core.entity.FileDescriptor;
import
com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.export.ExportDisplay;
import com.haulmont.cuba.gui.export.ExportFormat;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import javax.inject.Inject;
import java.util.Map;
public class EmployeeEdit extends
AbstractEditor<Employee> {
@Inject
private DataSupplier dataSupplier;
private DataSupplier dataSupplier;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private ExportDisplay exportDisplay;
@Inject
private FileUploadField uploadField;
@Inject
private Button downloadImageBtn;
@Inject
private Button clearImageBtn;
@Inject
private Datasource<Employee> employeeDs;
@Inject
private Image image;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event
-> {
FileDescriptor fd =
uploadField.getFileDescriptor();
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFil
eId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error
saving file to FileStorage", e);
}
getItem().setImageFile(dataSupplier.commit(fd));
displayImage();
});
uploadField.addFileUploadErrorListener(event -
>
>
showNotification("File upload error",
NotificationType.HUMANIZED));
employeeDs.addItemPropertyChangeListener(event
-> {
if
("imageFile".equals(event.getProperty()))
updateImageButtons(event.getValue() !=
null);
});
}
@Override
protected void postInit() {
displayImage();
updateImageButtons(getItem().getImageFile() !=
null);
}
public void onDownloadImageBtnClick() {
if (getItem().getImageFile() != null)
exportDisplay.show(getItem().getImageFile(),
ExportFormat.OCTET_STREAM);
}
public void onClearImageBtnClick() {
getItem().setImageFile(null);
displayImage();
}
private void updateImageButtons(boolean enable) {
downloadImageBtn.setEnabled(enable);
clearImageBtn.setEnabled(enable);
}
В методе init() сначала инициализируется компонент
uploadField, предназначенный для загрузки новой
фотографии. В случае успешной загрузки из
компонента получается экземпляр нового
FileDescriptor, и соответствующий файл отправляется
из временного хранилища в постоянное вызовом
FileUploadingAPI.putFileIntoStorage(). После этого
FileDescriptor сохраняется в БД вызовом
DataSupplier.commit(), и сохраненный экземпляр
устанавливается в атрибуте imageFile редактируемой
сущности Employee. Затем вызывается метод
displayImage() контроллера для отображения
загруженной фотографии.
Далее в методе init() источнику данных, содержащему
редактируемый экземпляр Employee, добавляется
слушатель для запрещения или разрешения кнопок
выгрузки и очистки файла в зависимости от того,
загружен файл или нет.
Метод postInit() вызывает отображение файла и
обновляет состояние кнопок в зависимости от наличия
загруженного файла.
Метод onDownloadImageBtnClick() вызывается при
private void displayImage() {
if (getItem().getImageFile() != null) {
image.setSource(FileDescriptorResource.class).setFileD
escriptor(getItem().getImageFile());
image.setVisible(true);
} else {
image.setVisible(false);
}
}
}
Метод onDownloadImageBtnClick() вызывается при
нажатии кнопки downloadImageBtn и выполняет
выгрузку файла с помощью интерфейса ExportDisplay.
Метод onClearImageBtnClick() вызывается при
нажатии кнопки clearImageBtn и очищает атрибут
imageFile сущности Employee. Удаления файла из
хранилища не производится.
Метод displayImage() выгружает файл из хранилища и
устанавливает его в качестве содержимого компонента
image.
4.5.2.1. 4.5.2.1. Вывод изображений в
колонках таблицы
Расширим задачу из предыдущего примера, настроив
отображение фотографий на экране просмотра списка
сотрудников.
Изображения можно отобразить как в отдельной колонке,
так и внутри существующих колонок. В обоих случаях
будет использоваться интерфейс Table.ColumnGenerator.
Фрагмент XML-дескриптора экрана просмотра списка
Employee:
<groupTable id="employeesTable"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<columns>
<column id="name"/>
</columns>
<rows datasource="employeesDs"/>
Чтобы отображать фотографию рядом с именем
сотрудника в колонке name, необходимо изменить
стандартное представление данных в этой колонке.
Например, можно использовать контейнер HBoxLayout,
поместив внутрь него компонент Image:
<rows datasource="employeesDs"/>
<rowsCount/>
<buttonsPanel id="buttonsPanel"
alwaysVisible="true">
<button id="createBtn"
action="employeesTable.create"/>
<button id="editBtn"
action="employeesTable.edit"/>
<button id="removeBtn"
action="employeesTable.remove"/>
</buttonsPanel>
</groupTable>
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import
com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import javax.inject.Inject;
import java.util.Map;
import static
com.haulmont.cuba.gui.components.Image.*;
public class EmployeeBrowse extends AbstractLookup {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private GroupTable<Employee> employeesTable;
@Override
public void init(Map<String, Object> params) {
employeesTable.addGeneratedColumn("name",
entity -> {
Image image =
componentsFactory.createComponent(Image.class);
image.setScaleMode(ScaleMode.CONTAIN);
image.setHeight("40");
image.setWidth("40");
FileDescriptor userImageFile =
entity.getImageFile();
image.setSource(FileDescriptorResource.class).setFileD
escriptor(userImageFile);
Label userLogin =
componentsFactory.createComponent(Label.class);
userLogin.setValue(entity.getName());
userLogin.setAlignment(Alignment.MIDDLE_LEFT);
HBoxLayout hBox =
componentsFactory.createComponent(HBoxLayout.class);
hBox.setSpacing(true);
hBox.add(image);
hBox.add(userLogin);
return hBox;
});
}
}
В методе init() вызывается метод
addGeneratedColumn(), который принимает два
параметра: идентификатор колонки и реализацию
интерфейса Table.ColumnGenerator, с помощью
которого мы зададим своё представление для колонки
name.
В этом методе мы создадим компонент Image, пользуясь
интерфейсом ComponentsFactory. Укажем режим
масштабирования компонента (CONTAIN) и зададим его
размеры.
Затем получим экземпляр FileDescriptor с
изображением, которое хранится в File Storage. Ссылка на
это изображение хранится в атрибуте imageFile
сущности Employee. Используем тип ресурса
FileDescriptorImageResource, чтобы задать источник
данных для компонента Image.
Атрибут name можно отобразить как компонент Label
рядом с изображением.
Оба компонента Image и Label обернём в контейнер
HBoxLayout, который будет возвращать метод
addGeneratedColumn() в качестве новой разметки
ячейки.
Можно использовать также более декларативный подход
Можно использовать также более декларативный подход
с атрибутом generator.
4.5.3. 4.5.3. Отправка email email
В данном разделе рассматривается пример
использования механизма рассылки email.
Рассмотрим следующую задачу:
Имеется сущность NewsItem и экран ее редактирования
NewsItemEdit.
Сущность NewsItem имеет следующие атрибуты: date,
caption, content.
Необходимо отсылать электронные письма каждый раз,
когда через экран NewsItemEdit создается новый
экземпляр сущности. Email должен содержать
NewsItem.caption в качестве темы письма, тело письма
должно формироваться на основе шаблона,
включающего NewsItem.content.
1. Добавьте следующий код в NewsItemEdit.java:
public class NewsItemEdit extends
AbstractEditor<NewsItem> {
// Indicates that a new item was created in this
editor
private boolean justCreated;
@Inject
protected EmailService emailService;
// This method is invoked when a new item is
initialized
@Override
protected void initNewItem(NewsItem item) {
justCreated = true;
}
// This method is invoked after the screen
commit
@Override
protected boolean postCommit(boolean committed,
boolean close) {
if (committed && justCreated) {
// If a new entity was saved to the
database, ask a user about sending an email
showOptionDialog(
"Email",
"Send the news item by email?",
MessageType.CONFIRMATION,
new Action[] {
new
DialogAction(DialogAction.Type.YES) {
@Override
public void
actionPerform(Component component) {
sendByEmail();
}
},
new
DialogAction(DialogAction.Type.NO)
}
);
}
return super.postCommit(committed, close);
}
// Queues an email for sending asynchronously
private void sendByEmail() {
NewsItem newsItem = getItem();
EmailInfo emailInfo = new EmailInfo(
"john.doe@company.com,jane.roe@company.com",
//
Как видно, метод sendByEmail() вызывает сервис
EmailService и передает ему экземпляр EmailInfo,
описывающий сообщение. Тело сообщений будет
создаваться на основе шаблона news_item.txt.
2. Создайте шаблон тела письма в файле news_item.txt в
пакете com.company.demo.templates модуля corecore:
Это шаблон Freemarker, который использует параметры,
переданные в EmailInfo (в данном случае
единственный параметр newsItem).
3. Запустите приложение, откройте браузер сущности
NewsItem и нажмите CreateCreate. Откроется экран
редактирования сущности. Заполните поля и нажмите
OKOK. Появится диалог подтверждения с вопросом об
отсылке email. Нажмите YesYes.
Перейдите в экран Administration > Email HistoryAdministration > Email History вашего
"john.doe@company.com,jane.roe@company.com",
//
recipients
newsItem.getCaption(),
// subject
null,
// the "from" address will be
taken from the "cuba.email.fromAddress" app property
"com/company/demo/templates/news_item.txt",
// body
template
Collections.singletonMap("newsItem", newsItem)
//
template parameters
);
emailService.sendEmailAsync(emailInfo);
}
}
The company news:
${newsItem.content}
4. Перейдите в экран Administration > Email HistoryAdministration > Email History вашего
приложения. Вы увидите две записи (по числу
получателей) со статусом Queue. Он означает, что
сообщения находятся в очереди и еще не отосланы.
5. Для обработки очереди необходимо создать
назначенное задание. Перейдите в экран Administration >Administration >
Scheduled TasksScheduled Tasks вашего приложения. Создайте новую
задачу и установите ей следующие параметры:
Bean NameBean Name - cuba_Emailer
Method NameMethod Name - processQueuedEmails()
SingletonSingleton - да (этот параметр важен только при
эксплуатации кластера серверов middleware)
Period, secPeriod, sec - 10
Сохраните задачу и нажмите на ней ActivateActivate.
Если вы ранее не настраивали выполнение
назначенных заданий для данного приложения ранее,
то на данном этапе ничего не произойдет - новая задача
не начнет выполняться пока вы не запустите весь
механизм назначенных заданий.
6. Откройте файл modules/core/src/app.properties и
добавьте в него следующее свойство:
Перезапустите сервер приложения. Механизм
выполнения заданий теперь активен и вызывает
обработку очереди email.
7. Перейдите в экран Administration > Email HistoryAdministration > Email History. Статус
сообщений будет либо Sent, если они успешно
отосланы, либо, что более вероятно, Sending или Queue,
если произошла ошибка отправки. В последнем случае
вы можете открыть журнал приложения в файле
build/tomcat/logs/app.log и выяснить причину.
cuba.schedulingActive = true
build/tomcat/logs/app.log и выяснить причину.
Механизм отсылки email предпримет несколько (по
умолчанию 10) попыток и в случае неудачи переведет
сообщения в статус Not sent.
8. Наиболее очевидной причиной ошибки отправки
является то, что вы не настроили параметры SMTP-
сервера. Эти параметры могут быть заданы в базе
данных с помощью JMX бина app-
core.cuba:type=Emailer или в свойствах приложения
блока middleware. Рассмотрим второй способ. Откройте
файл modules/core/src/app.properties и добавьте в
него требуемые параметры:
Перезапустите сервер приложения. Перейдите в экран
Administration > JMX ConsoleAdministration > JMX Console, найдите JMX бин Emailer и
попробуйте послать самому себе тестовое сообщение с
помощью операции sendTestEmail().
9. Теперь механизм отсылки email настроен корректно,
однако он не будет отсылать сообщения, уже
переведенные в статус Not sent. Поэтому необходимо
создать новый экземпляр NewsItem через экран
редактирования. Сделайте это и понаблюдайте, как
статус новых сообщений в экране Email HistoryEmail History
изменится на Sent.
4.5.4. 4.5.4. Использование компонентов приложения
Как упоминалось в разделе Компоненты приложения,
любое CUBA-приложение может быть компонентом
другого приложения. Компонент приложения
представляет собой по сути full-stack библиотеку,
cuba.email.fromAddress = do-not-reply@company.com
cuba.email.smtpHost = mail.company.com
предоставляющую функциональность на всех уровнях - от
схемы БД до бизнес-логики и UI.
В данном разделе рассматривается пример создания
компонента приложения и использования его в проекте.
Компонент будет предоставлять функциональность
"Customer Management" и содержать сущность Customer и
соответствующие экраны UI. Приложение будет
использовать сущность Customer из компонента в
качестве ссылки в собственной сущности Order.
Создание компонента Customer Management Customer Management
1. Создайте новый проект в Studio и укажите следующие
значения в окне New projectNew project:
Project nameProject name - customers
Project namespaceProject namespace - cust
Root packageRoot package - com.company.customers
2. Откройте Project propertiesProject properties на редактирование и на
вкладке AdvancedAdvanced установите значение поля ModuleModule
prexprex в cust. Это необходимо для того, чтобы имена
артефактов компонента имели префикс, отличный от
app, принятого по умолчанию.
3. Создайте сущность Customer с атрибутом name.
Переключитесь на вкладку Instance nameInstance name и укажите
name в атрибутах name pattern.
Если компонент содержит персистентные классы,
аннотированные @MappedSuperclass, убедитесь,
что в этом же проекте есть их наследники,
что в этом же проекте есть их наследники,
являющиеся сущностями (т.е. аннотированные
@Entity). В противном случае байткод таких
базовых классов не будет необходимым образом
модифицирован и они не будут правильно
работать в приложениях, использующих
компонент.
4. Сгененрируйте скрипты БД и создайте стандартные
экраны для сущности Customer: cust$Customer.browse
и cust$Customer.edit. После этого откройте дизайнер
меню и переименуйте пункт application в
customerManagement.
5. Нажмите на ссылку App component descriptorApp component descriptor на панели
Project propertiesProject properties. Сохраните сгенерированный
описатель компонента нажав OKOK.
6. Проверьте функциональность Customer Management: RunRun
> Create database> Create database, Run > Start application serverRun > Start application server, затем
откройте http://localhost:8080/cust в веб-
браузере.
7. Установите компонент приложения в локальный
Maven-репозиторий, выполнив команду главного меню
Run > Install app componentRun > Install app component. Данная команда просто
запускает задачу Gradle install после остановки
демонов Gradle.
Создание приложения Sales Sales
1. Создайте новый проект в Studio и укажите следующие
значения в окне New projectNew project:
Project nameProject name - sales
Project namespaceProject namespace - sales
Root packageRoot package - com.company.sales
2. Откройте Project propertiesProject properties на редактирование и на
панели App componentsApp components нажмите на кнопку добавления
Custom componentsCustom components. В диалоге Custom applicationCustom application
Custom componentsCustom components. В диалоге Custom applicationCustom application
componentcomponent выберите проект customers в списке
Registered projectRegistered project. Данный список содержит все проекты,
зарегистрированные в Studio и имеющие описатель
app-component.xml. Нажмите в диалоге OKOK. В списке
кастомных компонентов проекта появятся Maven-
координаты компонента Customer Management.
Сохраните страницу свойств проекта нажатием OKOK.
3. Создайте сущность Order с атрибутами date и amount.
Добавьте атрибут customer в виде many-to-one
ассоциации с сущностью Customer - она дожна быть
доступна в выпадающем списке TypeType.
4. Сгенерируйте скрипты БД и создайте стандартные
экраны для сущности Order. При создании экранов
создайте представление order-with-customer-view,
включающее атрибут customer и используйте его в
экранах.
5. Проверьте функциональность приложения: Run > CreateRun > Create
databasedatabase, Run > Start application serverRun > Start application server, затем откройте
http://localhost:8080/app в веб-браузере.
Приложение должно содержать два пункта меню
верхнего уровня: Customer ManagementCustomer Management и ApplicationApplication.
Модификация компонента Customer Management Customer Management
Предположим, что необходимо изменить
функциональность компонента (добавить атрибут в
сущность Customer) и пересобрать приложение для
внесения этих изменений.
1. Откройте проект customers в Studio.
2. Откройте сущность Customer на редактирование и
добавьте атрибут address. При сохранении изменений
выберите экраны браузера и редактора для
включения нового атрибута.
3. Сгенерируйте скрипты БД - будет создано новый
скрипт обновления с изменением таблицы. Сохраните
скрипты.
4. Проверьте изменения в компоненте: Run > UpdateRun > Update
databasedatabase, Run > Start application serverRun > Start application server, затем откройте
http://localhost:8080/cust в веб-браузере.
5. Переинсталлируйте компонент в локальный Maven-
репозиторий выполнив команду меню Run > Install appRun > Install app
componentcomponent.
6. Закройте проект customers и откройте sales.
7. Выполните команды меню Build > CleanBuild > Clean, затем Build >Build >
Assemble projectAssemble project.
8. Запустите Run > Update databaseRun > Update database - будет выполнен
скрипт обновления из компонента Customer
Management.
9. Выполните Run > Start application serverRun > Start application server и откройте
http://localhost:8080/app в веб-браузере -
приложение теперь содержит сущность Customer и
соответствующие экраны с атрибутом address.
Предоставление общего доступа к компоненту Customer Management Customer Management
Компонент приложения можно выгрузить в удаленный
Maven-репозиторий.
1. Остановите сервер Studio.
2. Установите репозиторий как описано в разделе
Установка приватного репозитория артефактов.
3. Откройте build.gradle проекта customers в
текстовом редакторе. Замените репозиторий и
имя/пароль пользователя в секции
buildscript/repositories и добавьте
uploadRepository в секции cuba:
buildscript {
...
4. Откройте командную строку в корневом каталоге
проекта customers и выполните gradle assemble. Это
необходимо для того, чтобы новый репозиторий
закэшировал артефакты CUBA, требуемые для работы
Studio.
5. В окне сервера Studio укажите ваш репозиторий и
имя/пароль для доступа к нему вместо стандартного
репозитория CUBA. Запустите сервер Studio.
repositories {
maven {
url
'http://repo.company.com/nexus/content/groups/work
'
// repository containing CUBA and your own
artifacts
credentials {
username(rootProject.hasProperty('repoUser') ?
rootProject['repoUser'] : 'admin')
password(rootProject.hasProperty('repoPass') ?
rootProject['repoPass'] : 'admin123')
}
}
...
cuba {
...
uploadRepository {
url =
'http://repo.company.com/nexus/content/repositorie
s/snapshots'
// repository for uploading your
artifacts
user = 'admin'
password = 'admin123'
}
}
репозитория CUBA. Запустите сервер Studio.
6. Откройте проект customers в Studio.
7. Найдите в диалоге SearchSearch (Alt-/) задачу Gradle
uploadArchives и запустите ее. Эту задачу можно
запускать и из командной строки. Артефакты
компонента Customer Management будет выгружены в
ваш репозиторий.
8. Удалите артефакты компонента из локального Maven-
репозитория чтобы быть уверенным, что они
загрузятся из удаленного репозитория при
последующей сборке приложения sales: просто
удалите каталог .m2/repository/com/company,
находящийся в домашнем каталоге пользователя.
9. Откройте проект customers в Studio. Адрес
репозитория в build.gradle будет автоматически
заменен на тот, который указан в окне сервера Studio.
10. Теперь можно собрать и запустить приложение -
компонент Customer Management будет загружен из
удаленного репозитория.
4.5.5. 4.5.5. Создание собственных визуальных
компонентов
В разделе Собственные визуальные компоненты был
приведен обзор методов расширения набора стандартных
визуальных компонентов в проекте. У вас есть
следующие варианты:
1. Подключение аддона Vaadin. Много сторонних
компонентов Vaadin распространяются в виде
дополнений (add-on). Библиотека аддонов находится по
адресу https://vaadin.com/directory.
2. Подключение компонента, написанного на JavaScript.
Vaadin дает возможность создавать серверные
компоненты, использующие JavaScript-библиотеку.
компоненты, использующие JavaScript-библиотеку.
3. Создание собственного компонента Vaadin с клиентской
частью, написанной на GWT.
Далее вы можете интегрировать получившийся
компонент Vaadin в универсальный пользовательский
интерфейс CUBA, чтобы иметь возможность использовать
его декларативно в XML-дескрипторах экранов и
привязывать к источникам данных.
Финальным шагом интеграции является поддержка
нового компонента в WYSIWYG редакторе экранов Studio.
Далее в этом разделе приводятся примеры создания
новых визуальных компонентов каждым из описанных
выше способов. Интеграция в Generic UI одинакова для
всех трех способов, поэтому она описана только для
примера с подключением аддона Vaadin.
4.5.5.1. 4.5.5.1. Подключение аддона Vaadin Vaadin
Рассмотрим пример использования компонента StepperStepper,
доступного по адресу http://vaadin.com/addon/stepper.
Данный компонент позволяет пошагово изменять
значение текстового поля с помощью клавиатуры,
колесика мыши и встроенных кнопок вверх/вниз.
Создайте новый проект в CUBA Studio и назовите его
addon-demo.
Для подключения аддона Vaadin проект должен иметь
модуль web-toolkitweb-toolkit. Создайте его, нажав на ссылку Create webCreate web
toolkit moduletoolkit module секции Project propertiesProject properties навигатора.
Далее нажмите на ссылку New UI componentNew UI component. Откроется
окно создания визуального компонента UI componentUI component
generationgeneration. В секции Component typeComponent type выберите значение
Vaadin add-on.
Заполните следующие поля:
Add-on Maven dependencyAdd-on Maven dependency - в этом поле необходимо указать
Maven-координаты аддона Vaadin для подключения его
как зависимости к текущему проекту. Указание
координат возможно в двух форматах:
1. XML, скопированный с сайта аддона
(http://vaadin.com/addon/stepper):
2. Одной строкой в том виде, как вы добавляете
зависимости в build.gradle:
org.vaadin.addons:stepper:2.2.2
<dependency>
<groupId>org.vaadin.addons</groupId>
<artifactId>stepper</artifactId>
<version>2.2.2</version>
</dependency>
org.vaadin.addons:stepper:2.2.2
Inherited widgetsetInherited widgetset - в этом поле необходимо указать имя
виджетсета подключаемого аддона:
Integrate into generic UIIntegrate into generic UI - в данном примере флажок должен
быть снят, т.к. мы не интегрируем компонент в
универсальный интерфейс платформы.
Нажмите кнопку OKOK.
Если открыть проект в IDE, то можно увидеть, что Студия
изменила два файла:
1. build.gradle. В модуле webweb появилась новая зависимость
от аддона, содержащего компонент:
2. В файл AppWidgetSet.gwt.xml модуля web-toolkitweb-toolkit проекта
подключен виджетсет аддона после виджетсета
платформы:
org.vaadin.risto.stepper.widgetset.StepperWidgetset
configure(webModule) {
...
dependencies {
...
compile("org.vaadin.addons:stepper:2.2.2")
}
<module>
<inherits
name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" />
<inherits
name="org.vaadin.risto.stepper.widgetset.StepperWidg
etset" />
Для более быстрой сборки виджетов на время
разработки вы можете установить свойство
user.agent. В данном примере набор виджетов
будет собираться только для браузеров,
основанных на WebKit: Chrome, Safari, и т.д.
Компонент из аддона Vaadin подключен. Далее мы
покажем как использовать его в экранах проекта.
Создаем новую сущность Customer с двумя полями:
name типа String
score типа Integer
Сгенерируем для новой сущности стандартные экраны.
В диалоге генерации стандартных экранов убедитесь
что значение поля In moduleIn module - Web Module. Экраны,
использующие компоненты Vaadin напрямую, должны
располагаться в модуле webweb.
На самом деле экран может располагаться и в
модуле guigui, но тогда код, работающий с Vaadin
компонентом, должен быть вынесен в отдельный
компаньон.
Далее добавим компонент stepper на экран. Вы можете
поместить его как в FieldGroup, так и вне ее. Рассмотрим
оба способа.
1. В XML-дескрипторе экрана редактирования customer-
edit.xml для поля score компонента fieldGroup
добавим атрибут custom = "true":
etset" />
<set-property name="user.agent" value="safari"
/>
В контроллер экрана редактирования
CustomerEdit.java добавим следующий код:
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addondemo.web.customer.CustomerE
dit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addondemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addondemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
<field property="score"
custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
package com.company.addondemo.web.customer;
import
com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.addondemo.entity.Customer;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import
com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import
com.haulmont.cuba.web.gui.components.WebComponentsHe
lper;
import com.vaadin.ui.Layout;
import org.vaadin.risto.stepper.IntStepper;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends
AbstractEditor<Customer> {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
private IntStepper stepper = new IntStepper();
@Override
public void init(Map<String, Object> params) {
fieldGroup.createField("score");
Здесь в поле stepper создается экземпляр компонента,
подключенного из аддона. В методе init()
производится инициализация кастомного поля score.
Через ComponentsFactory создается экземпляр
BoxLayout, затем из него с помощью WebComponentsHelper
извлекается ссылка на Vaadin-контейнер, и в этот
контейнер добавляется наш новый компонент.
BoxLayout возвращается для отображения в кастомном
поле.
fieldGroup.createField("score");
Component box =
componentsFactory.createComponent(VBoxLayout.class);
fieldGroup.getFieldNN("score").setComponent(box);
Layout layout = (Layout)
WebComponentsHelper.unwrap(box);
layout.addComponent(stepper);
stepper.setSizeFull();
stepper.addValueChangeListener(event ->
customerDs.getItem().setValue("score",
event.getProperty().getValue())
);
}
@Override
protected void initNewItem(Customer item) {
item.setScore(0);
}
@Override
protected void postInit() {
stepper.setValue(getItem().getScore());
}
}
поле.
Для связи компонента с данными во-первых, в методе
postInit() ему устанавливается текущее значение из
редактируемого Customer, а во-вторых, добавляется
слушатель на изменение значения, который обновляет
соответствующий атрибут сущности при изменении
значения пользователем.
2. Чтобы использовать новый компонент вне FieldGroup в
произвольном месте экрана в XML-дескрипторе
объявим контейнер scoreBox и удалим поле score из
fieldGroup:
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addondemo.web.customer.CustomerE
dit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addondemo.web.customer">
<dsContext>
<datasource id="customerDs"
class="com.company.addondemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
В контроллере инжектируем контейнер, извлекаем
ссылку на Vaadin-контейнер и добавляем в него
компонент:
</column>
</fieldGroup>
<hbox id="scoreBox" spacing="true">
<label value="Score"
align="MIDDLE_LEFT"/>
</hbox>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
package com.company.addondemo.web.customer;
import com.haulmont.cuba.gui.components.*;
import com.company.addondemo.entity.Customer;
import com.haulmont.cuba.gui.data.Datasource;
import
com.haulmont.cuba.web.gui.components.WebComponentsHe
lper;
import com.vaadin.ui.Layout;
import org.vaadin.risto.stepper.IntStepper;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends
AbstractEditor<Customer> {
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
@Inject
private BoxLayout scoreBox;
private IntStepper stepper = new IntStepper();
@Override
public void init(Map<String, Object> params) {
Layout box = (Layout)
WebComponentsHelper.unwrap(scoreBox);
box.addComponent(stepper);
fieldGroup.addField(fieldGroup.createField("score")
);
stepper.setSizeFull();
stepper.addValueChangeListener(event ->
customerDs.getItem().setValue("score",
event.getProperty().getValue())
);
}
@Override
protected void initNewItem(Customer item) {
item.setScore(0);
}
@Override
protected void postInit() {
stepper.setValue(getItem().getScore());
}
Связь с данными выполняется здесь аналогично
примеру с FieldGroup.
Для адаптации внешнего вида компонента создадим в
проекте расширение темы. Для этого в Studio выполним
команду Create theme extensionCreate theme extension секции Project propertiesProject properties
навигатора. В списке тем для расширения выберем halo
и нажмем кнопку CreateCreate. Затем откроем файл
themes/halo/halo-ext.scss модуля webweb, и добавим в
него следующий код:
Запускаем сервер приложения. Экран редактирования
должен выглядеть следующим образом:
}
}
@import "../halo/halo";
/* Define your theme modifications inside next mixin
*/
@mixin halo-ext {
@include halo;
/* Basic styles for stepper inner text box */
.stepper input[type="text"] {
@include box-defaults;
@include valo-textfield-style;
&:focus {
@include valo-textfield-focus-style;
}
}
}
4.5.5.2. 4.5.5.2. Подключение аддона Vaadin Vaadin с
интеграцией в Generic UI Generic UI
В предыдущем разделе мы подключили в проект
сторонний компонент Stepper. В данном разделе мы
интегрируем его в универсальный пользовательский
интерфейс CUBA, что даст разработчикам возможность
использовать компонент декларативно в XML-
дескрипторах экранов и связывать его с сущностями
через источники данных.
Создадим новый проект в CUBA Studio и назовем его addon-
gui-demo. В поле Project namespaceProject namespace введем значение agd.
Создадим модуль web-toolkitweb-toolkit, нажав на кнопку Create webCreate web
toolkit moduletoolkit module секции Project propertiesProject properties навигатора Studio.
Далее нажимаем на кнопку New UI componentNew UI component. Откроется
окно создания визуального компонента UI componentUI component
generationgeneration. В секции Component typeComponent type выбираем значение
Vaadin add-on.
Заполним поля Add-on Maven dependencyAdd-on Maven dependency и Inherited widgetsetInherited widgetset
как описано в предыдущем разделе.
Далее заполним поля в нижней секции:
Integrate into Generic UIIntegrate into Generic UI указывает на необходимости
интеграции компонента в универсальный
пользовательский интерфейс платформы.
Component XML elementComponent XML element - имя элемента компонента в XML-
дескрипторе экрана. Введите значение stepper.
Component interface nameComponent interface name - имя интерфейса компонента
для универсального UI платформы. Введите Stepper.
FQN of Vaadin component from add-onFQN of Vaadin component from add-on - полное имя класса
компонента Vaadin из аддона. В нашем случае это
org.vaadin.risto.stepper.IntStepper.
После нажатия кнопки OKOK Studio сделает следующее:
Добавит аддон Vaadin в зависимости модуля webweb в файле
build.gradle.
Подключит виджетсет аддона в файле
AppWidgetSet.gwt.xml модуля web-toolkitweb-toolkit.
Сгенерирует заготовки для следующих файлов:
Stepper - интерфейс компонента в модуле guigui.
WebStepper - реализация компонента в модуле webweb.
StepperLoader - XML-загрузчик компонента в модуле guigui.
ui-component.xsd - описатель схемы XML для нового
компонента. Если файл уже существовал на момент
генерации компонента, то информация о новом
компоненте будет добавлена в существующий файл.
cuba-ui-component.xml - файл регистрации загрузчика
нового компонента в модуле webweb. Если файл
существовал, то информация о новом компоненте будет
добавлена в существующий файл.
Откройте проект в IDE.
Последовательно пройдемся по сгенерированным Studio
заготовкам файлов и внесем в них необходимые
изменения.
Перейдите к интерфейсу Stepper в модуле guigui. Замените
его содержимое на следующий код:
package com.company.addonguidemo.gui.components;
import com.haulmont.cuba.gui.components.Field;
public interface Stepper extends Field {
String NAME = "stepper";
boolean isManualInputAllowed();
void setManualInputAllowed(boolean value);
boolean isMouseWheelEnabled();
void setMouseWheelEnabled(boolean value);
int getStepAmount();
void setStepAmount(int amount);
int getMaxValue();
void setMaxValue(int maxValue);
В качестве базового для нашего компонента выбран
интерфейс Field. Это позволяет осуществить связь с
данными (data binding), то есть отображать и
редактировать значение некоторого атрибута
сущности.
Далее перейдите к классу WebStepper - реализации
компонента в модуле webweb. Замените содержимое класса
следующим кодом:
void setMaxValue(int maxValue);
int getMinValue();
void setMinValue(int minValue);
}
package com.company.addonguidemo.web.gui.components;
import
com.company.addonguidemo.gui.components.Stepper;
import
com.haulmont.cuba.web.gui.components.WebAbstractFiel
d;
import org.vaadin.risto.stepper.IntStepper;
public class WebStepper extends
WebAbstractField<IntStepper> implements Stepper {
public WebStepper() {
this.component = new
org.vaadin.risto.stepper.IntStepper();
}
@Override
public boolean isManualInputAllowed() {
return component.isManualInputAllowed();
}
@Override
@Override
public void setManualInputAllowed(boolean value)
{
component.setManualInputAllowed(value);
}
@Override
public boolean isMouseWheelEnabled() {
return component.isMouseWheelEnabled();
}
@Override
public void setMouseWheelEnabled(boolean value)
{
component.setMouseWheelEnabled(value);
}
@Override
public int getStepAmount() {
return component.getStepAmount();
}
@Override
public void setStepAmount(int amount) {
component.setStepAmount(amount);
}
@Override
public int getMaxValue() {
return component.getMaxValue();
}
@Override
public void setMaxValue(int maxValue) {
component.setMaxValue(maxValue);
}
@Override
public int getMinValue() {
return component.getMinValue();
}
В качестве базового класса выбран WebAbstractField,
который реализует логику интерфейса Field.
StepperLoader в модуле guigui загружает компонент из его
представления в XML.
}
@Override
public void setMinValue(int minValue) {
component.setMinValue(minValue);
}
}
package
com.company.addonguidemo.gui.xml.layout.loaders;
import
com.company.addonguidemo.gui.components.Stepper;
import
com.haulmont.cuba.gui.xml.layout.loaders.AbstractFie
ldLoader;
public class StepperLoader extends
AbstractFieldLoader<Stepper> {
@Override
public void createComponent() {
resultComponent =
factory.createComponent(Stepper.class);
loadId(resultComponent, element);
}
@Override
public void loadComponent() {
super.loadComponent();
String manualInput =
String manualInput =
element.attributeValue("manualInput");
if (manualInput != null) {
resultComponent.setManualInputAllowed(Boolean.parseB
oolean(manualInput));
}
String mouseWheel =
element.attributeValue("mouseWheel");
if (mouseWheel != null) {
resultComponent.setMouseWheelEnabled(Boolean.parseBo
olean(mouseWheel));
}
String stepAmount =
element.attributeValue("stepAmount");
if (stepAmount != null) {
resultComponent.setStepAmount(Integer.parseInt(stepA
mount));
}
String maxValue =
element.attributeValue("maxValue");
if (maxValue != null) {
resultComponent.setMaxValue(Integer.parseInt(maxValu
e));
}
String minValue =
element.attributeValue("minValue");
if (minValue != null) {
resultComponent.setMinValue(Integer.parseInt(minValu
e));
}
}
}
Логика загрузки базовых свойств компонента Field
сосредоточена в классе AbstractFieldLoader. Нам
достаточно загрузить только специфические свойства
Stepper.
В файле cuba-ui-component.xml, расположенном в
корне модуля webweb, регистрируется новый компонент и
его загрузчик. Оставляем файл без изменений.
Файл ui-component.xsd, расположенный в корне модуля
guigui, это описатель XML схемы новых компонентов
проекта. Добавим к элементу stepper описание его
атрибутов.
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<components>
<component
xmlns="http://schemas.haulmont.com/cuba/components.x
sd">
<name>stepper</name>
<componentLoader>com.company.addonguidemo.gui.xml.la
yout.loaders.StepperLoader</componentLoader>
<class>com.company.addonguidemo.web.gui.components.W
ebStepper</class>
</component>
</components>
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<xs:schema
xmlns="http://schemas.company.com/agd/0.1/ui-
xmlns="http://schemas.company.com/agd/0.1/ui-
component.xsd"
attributeFormDefault="unqualified"
elementFormDefault="qualified"
targetNamespace="http://schemas.company.com/agd/0.1/
ui-component.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="stepper">
<xs:complexType>
<xs:attribute name="id"
type="xs:string"/>
<xs:attribute name="caption"
type="xs:string"/>
<xs:attribute name="width"
type="xs:string"/>
<xs:attribute name="height"
type="xs:string"/>
<xs:attribute name="datasource"
type="xs:string"/>
<xs:attribute name="property"
type="xs:string"/>
<xs:attribute name="manualInput"
type="xs:boolean"/>
<xs:attribute name="mouseWheel"
type="xs:boolean"/>
<xs:attribute name="stepAmount"
type="xs:int"/>
<xs:attribute name="maxValue"
type="xs:int"/>
<xs:attribute name="minValue"
type="xs:int"/>
</xs:complexType>
</xs:element>
</xs:schema>
Далее рассмотрим, как добавить новый компонент на
экран.
Создадим новую сущность Customer с двумя полями:
name типа String
score типа Integer
Сгенерируем для новой сущности стандартные экраны.
Далее добавим компонент stepper на экран. Вы можете
поместить его как в FieldGroup, так и в отдельный
контейнер. Рассмотрим оба способа.
1. Использование компонента в экране внутри
произвольного контейнера.
Откройте файл customer-edit.xml.
Объявите новое пространство имен
xmlns:app="http://schemas.company.com/agd/0.1/ui-
component.xsd"
Удалите поле score из fieldGroup.
Добавьте компонент stepper на экран.
В результате XML-дескриптор редактора должен
выглядеть так:
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:app="http://schemas.company.com/agd/0.1/ui-
component.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.gui.customer.Custom
erEdit"
datasource="customerDs"
focusComponent="fieldGroup"
В данном примере компонент stepper подсоединен к
атрибуту score сущности Customer, экземпляр которой
находится в источнике данных customerDs.
2. Использование компонента в поле FieldGroup:
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.gui.customer
">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<app:stepper id="stepper"
datasource="customerDs" property="score"
caption="Score"
minValue="1" maxValue="20"/>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
<?xml version="1.0" encoding="UTF-8"
standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
caption="msg://editCaption"
class="com.company.addonguidemo.gui.customer.Custom
erEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.gui.customer
">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions" spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
<field property="score"
custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
package com.company.addonguidemo.web.customer;
import
com.company.addonguidemo.gui.components.Stepper;
import
com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.addonguidemo.entity.Customer;
Для адаптации внешнего вида компонента создадим в
проекте расширение темы. Для этого в Studio выполним
команду Create theme extensionCreate theme extension секции Project propertiesProject properties
навигатора. В списке тем для расширения выберем halo
и нажмем кнопку CreateCreate. Затем откроем файл
themes/halo/halo-ext.scss модуля webweb, и добавим в
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.data.Datasource;
import
com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import javax.inject.Inject;
import java.util.Map;
public class CustomerEdit extends
AbstractEditor<Customer> {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Customer> customerDs;
@Override
public void init(Map<String, Object> params) {
Stepper stepper =
componentsFactory.createComponent(Stepper.class);
stepper.setDatasource(customerDs, "score");
stepper.setWidth("100%");
fieldGroup.getFieldNN("score").setComponent(stepper
);
}
}
themes/halo/halo-ext.scss модуля webweb, и добавим в
него следующий код:
Запускаем сервер приложения. Экран редактирования
должен выглядеть следующим образом:
4.5.5.3. 4.5.5.3. Подключение JavaScript JavaScript
библиотеки
@import "../halo/halo";
/* Define your theme modifications inside next mixin
*/
@mixin halo-ext {
@include halo;
/* Basic styles for stepper inner text box */
.stepper input[type="text"] {
@include box-defaults;
@include valo-textfield-style;
&:focus {
@include valo-textfield-focus-style;
}
}
}
В данном примере мы подключим компонент Slider из
библиотеки jQuery UIjQuery UI. Слайдер будет иметь два ползунка,
определяющих диапазон значений.
Создайте новый проект в CUBA Studio и назовите его
jscomponent.
Далее нажмите на кнопку New UI componentNew UI component. Откроется
окно создания визуального компонента UI componentUI component
generationgeneration. В секции Component typeComponent type выберите значение
JavaScript component.
В поле Vaadin component class nameVaadin component class name диалога генерации
компонента введите значение SliderServerComponent.
Уберите флажок Integrate into Generic UIIntegrate into Generic UI. Процесс
интеграции компонента в универсальный интерфейс
аналогичен описанному в разделе Подключение аддона
Vaadin с интеграцией в Generic UI, поэтому рассматривать
его здесь мы не будем.
После нажатия кнопки OKOK Studio сгенерирует файлы:
SliderServerComponent - интегрированный с JavaScript
компонент Vaadin.
SliderState - класс состояния компонента Vaadin.
slider-connector.js - JavaScript коннектор для
компонента Vaadin
Последовательно пройдемся по сгенерированным
заготовкам файлов и внесем в них необходимые
изменения.
Класс состояния SliderState определяет, какие данные
будут пересылаться между сервером и клиентом. В
нашем случае мы будем пересылать информацию о
минимальном и максимальном возможном значении и
выбранных значениях слайдера.
Серверный компонент Vaadin SliderServerComponent.
package
com.company.jscomponent.web.toolkit.ui.slider;
import
com.vaadin.shared.ui.JavaScriptComponentState;
public class SliderState extends
JavaScriptComponentState {
public double[] values;
public double minValue;
public double maxValue;
}
package
com.company.jscomponent.web.toolkit.ui.slider;
import com.vaadin.annotations.StyleSheet;
import com.vaadin.ui.AbstractJavaScriptComponent;
import com.vaadin.annotations.JavaScript;
import elemental.json.JsonArray;
@JavaScript({"slider-connector.js", "jquery-ui.js"})
@StyleSheet({"jquery-ui.css"})
public class SliderServerComponent extends
AbstractJavaScriptComponent {
public interface ValueChangeListener {
void valueChanged(double[] newValue);
}
private ValueChangeListener listener;
public SliderServerComponent() {
addFunction("valueChanged", arguments -> {
JsonArray array = arguments.getArray(0);
double[] values = new double[2];
values[0] = array.getNumber(0);
values[1] = array.getNumber(1);
listener.valueChanged(values);
});
}
public void setValue(double[] value) {
getState().values = value;
}
public double[] getValue() {
return getState().values;
}
public double getMinValue() {
return getState().minValue;
}
public void setMinValue(double minValue) {
getState().minValue = minValue;
}
Серверный компонент Vaadin определяет набор
геттеров и сеттеров для работы с состоянием слайдера,
а также интерфейс слушателя изменения значений.
Класс компонента должен быть унаследован от
AbstractJavaScriptComponent.
Метод addFunction() в конструкторе класса объявляет
обработчик RPC-вызова функции valueChanged() с
клиента.
Аннотации @JavaScript и @StyleSheet указывают на
файлы, которые должны быть загружены на веб-
страницу. В нашем примере это JavaScript файлы
библиотеки jQuery UI и коннектора, а также файл со
public double getMaxValue() {
return getState().maxValue;
}
public void setMaxValue(double maxValue) {
getState().maxValue = maxValue;
}
@Override
protected SliderState getState() {
return (SliderState) super.getState();
}
public ValueChangeListener getListener() {
return listener;
}
public void setListener(ValueChangeListener
listener) {
this.listener = listener;
}
}
библиотеки jQuery UI и коннектора, а также файл со
стилями для jQuery UI. Расположим их в одном Java-
пакете с серверным компонентом.
Скачайте архив jQuery UI с сайта http://jqueryui.com/download и
поместите файлы jquery-ui.js и jquery-ui.css в пакет с
классом SliderServerComponent. На странице скачивания
jQuery UI у вас будет возможность выбрать компоненты,
которые будут помещены в архив. Для нашего примера
достаточно выделить пункт Slider группы Widgets.
JavaScript коннектор slider-connector.js.
com_company_jscomponent_web_toolkit_ui_slider_Slider
ServerComponent = function() {
var connector = this;
var element = connector.getElement();
$(element).html("<div/>");
$(element).css("padding", "5px 10px");
var slider = $("div", element).slider({
range: true,
slide: function(event, ui) {
Коннектор представляет собой функцию, которая при
загрузке веб-страницы проинициализирует JavaScript
компонент. Имя функции должно соответствовать
полному имени класса серверного компонента, где
точки в имени пакета заменены на символ
подчеркивания.
Vaadin добавляет ряд полезных методов в фунцию
коннектора. Например, this.getElement() возвращает
HTML DOM элемент компонента, this.getState()
возвращает объект-состояние.
В нашем примере коннектор делает следующее:
Инициализирует компонент slider из библиотеки jQuery
UI. При изменении положения одного из ползунков
будет вызывана функция slide(). Мы видим, что она в
свою очередь вызывает метод valueChanged()
коннектора. valueChanged() - это метод, который мы
объявили на стороне сервера в классе
SliderServerComponent.
Объявляет функцию onStateChange(). Она будет
connector.valueChanged(ui.values);
}
});
connector.onStateChange = function() {
var state = connector.getState();
slider.slider("values", state.values);
slider.slider("option", "min",
state.minValue);
slider.slider("option", "max",
state.maxValue);
$(element).width(state.width);
}
}
вызываться при изменении объекта-состояния на
стороне сервера.
Для демонстрации работы компонента создадим
сущность Product с тремя атрибутами:
name типа String
minDiscount типа Double
maxDiscount типа Double
Затем сгенерируем стандартные экраны для данной
сущности. Обратите внимание, что мы используем не
универсальный компонент платформы, а "нативный"
компонент Vaadin. Следовательно экраны должны
располагаться в модуле Web, а не в GUI - укажите это в
окне генерации стандартных экранов.
В редакторе сущности мы хотим устанавливать
минимальное и максимальное значение скидки с
помощью компонента slider, который мы только что
создали.
Перейдите к файлу product-edit.xml. Поля minDiscount и
maxDiscount сделайте нередактируемыми, добавив к
соответствующим элементам атрибут editable="false".
Затем добавьте в fieldGroup новое кастомное поле
slider.
В результате XML-дескриптор экрана редактирования
должен выглядеть следующим образом:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.jscomponent.web.product.ProductEdit
"
datasource="productDs"
Перейдите к файлу ProductEdit.java. Замените его
содержимое следующим кодом:
datasource="productDs"
focusComponent="fieldGroup"
messagesPack="com.company.jscomponent.web.product">
<dsContext>
<datasource id="productDs"
class="com.company.jscomponent.entity.Product"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="productDs">
<column width="250px">
<field property="name"/>
<field property="minDiscount"
editable="false"/>
<field property="maxDiscount"
editable="false"/>
<field id="slider" custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
package com.company.jscomponent.web.product;
import
com.company.jscomponent.web.toolkit.ui.slider.SliderSe
rverComponent;
rverComponent;
import
com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.jscomponent.entity.Product;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import
com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import
com.haulmont.cuba.web.gui.components.WebComponentsHelp
er;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
public class ProductEdit extends
AbstractEditor<Product> {
@Inject
private FieldGroup fieldGroup;
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Datasource<Product> productDs;
@Override
protected void initNewItem(Product item) {
super.initNewItem(item);
item.setMinDiscount(15.0);
item.setMaxDiscount(70.0);
}
@Override
protected void postInit() {
В методе initNewItem() мы проставляем начальные
значения скидок для нового продукта.
В методе init() инициализируем кастомное поле для
слайдера. Для компонента слайдера мы проставляем
текущие значения, максимальное и минимальное
значения, а также объявляем слушатель изменений
значений. При движении ползунка мы будем проставлять
новые значения скидок в соответвующие поля
редактируемой сущности.
Запустите проект и откройте экран редактирования
protected void postInit() {
super.postInit();
Component box =
componentsFactory.createComponent(VBoxLayout.class);
Layout vBox = (Layout)
WebComponentsHelper.unwrap(box);
SliderServerComponent slider = new
SliderServerComponent();
slider.setValue(new
double[]{getItem().getMinDiscount(),
getItem().getMaxDiscount()});
slider.setMinValue(0);
slider.setMaxValue(100);
slider.setWidth("240px");
slider.setListener(newValue -> {
getItem().setMinDiscount(newValue[0]);
getItem().setMaxDiscount(newValue[1]);
});
vBox.addComponent(slider);
fieldGroup.getFieldNN("slider").setComponent(box);
}
}
Запустите проект и откройте экран редактирования
продукта. Изменение положения ползунка на слайдере
должно изменять значение в соответствующем текстовом
поле.
4.5.5.4. 4.5.5.4. Создание GWT GWT компонента
В данном примере мы рассмотрим создание простого GWT-
компонента - поля рейтинга в виде 5 звезд, а также
использование его в экранах приложения.
Создадим новый проект в CUBA Studio. Имя проекта -
ratingsample.
Создайте модуль web-toolkitweb-toolkit, нажав на кнопку Create web-Create web-
toolkit moduletoolkit module секции Project propertiesProject properties навигатора Studio.
Далее нажмите на ссылку Create new UI componentCreate new UI component.
Откроется окно создания визуального компонента New UINew UI
componentcomponent. В секции Component typeComponent type выберите значение New
GWT component.
В поле Vaadin component class nameVaadin component class name диалога генерации
компонента введите значение
RatingFieldServerComponent.
Снимите флажок Integrate into Generic UIIntegrate into Generic UI. Процесс
интеграции компонента в универсальный интерфейс
аналогичен описанному в разделе Подключение аддона
Vaadin с интеграцией в Generic UI, поэтому рассматривать
его здесь мы не будем.
После нажатия кнопки OKOK Studio сгенерирует файлы:
RatingFieldWidget.java - виджет GWT в модуле web-web-
toolkittoolkit.
RatingFieldServerComponent.java - класс компонента
Vaadin.
RatingFieldState.java - класс состояния компонента.
RatingFieldConnector.java - коннектор, связывающий
клиентский код с серверным компонентом.
RatingFieldServerRpc.java - класс, определяющий API
сервера для клиентской части.
Последовательно рассмотрим сгенерированные студией
заготовки файлов и внесем в них необходимые
изменения.
GWT виджет RatingFieldWidget.java. Замените
содержимое файла на следующий код:
package
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FocusWidget;
import java.util.ArrayList;
import java.util.List;
public class RatingFieldWidget extends FocusWidget {
private static final String CLASSNAME =
"ratingfield";
// API for handle clicks
public interface StarClickListener {
void starClicked(int value);
}
protected List<SpanElement> stars = new
ArrayList<SpanElement>(5);
protected StarClickListener listener;
protected int value = 0;
public RatingFieldWidget() {
DivElement container =
DOM.createDiv().cast();
container.getStyle().setDisplay(Display.INLINE_BLOCK
);
for (int i = 0; i < 5; i++) {
SpanElement star =
DOM.createSpan().cast();
// add star element to the container
DOM.insertChild(container, star, i);
// subscribe on ONCLICK event
DOM.sinkEvents(star, Event.ONCLICK);
stars.add(star);
}
setElement(container);
setStylePrimaryName(CLASSNAME);
}
// main method for handling events in GWT
widgets
@Override
public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
switch (event.getTypeInt()) {
// react on ONCLICK event
case Event.ONCLICK:
SpanElement element =
event.getEventTarget().cast();
// if click was on the star
int index = stars.indexOf(element);
if (index >= 0) {
int value = index + 1;
// set internal value
setValue(value);
// notify listeners
if (listener != null) {
if (listener != null) {
listener.starClicked(value);
}
}
break;
}
}
@Override
public void setStylePrimaryName(String style) {
super.setStylePrimaryName(style);
for (SpanElement star : stars) {
star.setClassName(style + "-star");
}
updateStarsStyle(this.value);
}
// let application code change the state
public void setValue(int value) {
this.value = value;
updateStarsStyle(value);
}
// refresh visual representation
private void updateStarsStyle(int value) {
for (SpanElement star : stars) {
star.removeClassName(getStylePrimaryName() + "-star-
selected");
}
for (int i = 0; i < value; i++) {
stars.get(i).addClassName(getStylePrimaryName() + "-
star-selected");
Виджет представляет собой клиентский класс,
отвечающий за отображение компонента в веб-
браузере и реакцию на события. Он определяет
интерфейсы для работы с серверной частью. В нашем
случае это метод setValue() и интерфейс
StarClickListener.
Класс компонента Vaadin RatingFieldServerComponent.
Он определяет API для серверного кода, различные
get/set методы, слушатели событий и подключение
источников данных. Прикладные разработчики
используют в своём коде методы этого класса.
}
}
}
package com.company.ratingsample.web.toolkit.ui;
import
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield.RatingFieldServerRpc;
import
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield.RatingFieldState;
import com.vaadin.ui.AbstractField;
// the field will have a value with integer type
public class RatingFieldServerComponent extends
AbstractField<Integer> {
public RatingFieldServerComponent() {
// register an interface implementation that
will be invoked on a request from the client
registerRpc((RatingFieldServerRpc) value ->
setValue(value, true));
Класс состояния RatingFieldState отвечает за то,
какие данные будут пересылаться между клиентом и
setValue(value, true));
}
// field value type
@Override
public Class<? extends Integer> getType() {
return Integer.class;
}
// define own state class
@Override
protected RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
@Override
protected RatingFieldState getState(boolean
markAsDirty) {
return (RatingFieldState)
super.getState(markAsDirty);
}
// we need to refresh the state when setValue is
invoked from the application code
@Override
protected void setInternalValue(Integer
newValue) {
super.setInternalValue(newValue);
if (newValue == null) {
newValue = 0;
}
getState().value = newValue;
}
}
какие данные будут пересылаться между клиентом и
сервером. В нём определяются публичные поля,
которые будут автоматически сериализованы на
сервере и десериализованы на клиенте.
Интерфейс RatingFieldServerRpc — определяет API
сервера для клиентской части, его методы могут
вызываться с клиента при помощи механизма
удалённого вызова процедур, встроенного в Vaadin. Этот
интерфейс мы реализуем в самом компоненте, в данном
случае просто вызываем метод setValue() нашего
поля.
package
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield;
import com.vaadin.shared.AbstractFieldState;
public class RatingFieldState extends
AbstractFieldState {
{
// change the main style name of the
component
primaryStyleName = "ratingfield";
}
// define a field for the value
public int value = 0;
}
package
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield;
import com.vaadin.shared.communication.ServerRpc;
Коннектор RatingFieldConnector связывает
клиентский код с серверной частью.
public interface RatingFieldServerRpc extends
ServerRpc {
//method will be invoked in the client code
void starClicked(int value);
}
package
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield;
import
com.company.ratingsample.web.toolkit.ui.RatingFieldS
erverComponent;
import
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield.RatingFieldServerRpc;
import
com.company.ratingsample.web.toolkit.ui.client.ratin
gfield.RatingFieldState;
import
com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractFieldConnector;
import com.vaadin.shared.ui.Connect;
// link the connector with the server implementation
of RatingField
// extend AbstractField connector
@Connect(RatingFieldServerComponent.class)
public class RatingFieldConnector extends
AbstractFieldConnector {
// we will use a RatingFieldWidget widget
@Override
@Override
public RatingFieldWidget getWidget() {
RatingFieldWidget widget =
(RatingFieldWidget) super.getWidget();
if (widget.listener == null) {
widget.listener = new
RatingFieldWidget.StarClickListener() {
@Override
public void starClicked(int value) {
getRpcProxy(RatingFieldServerRpc.class).starClicked(
value);
}
};
}
return widget;
}
// our state class is RatingFieldState
@Override
public RatingFieldState getState() {
return (RatingFieldState) super.getState();
}
// react on server state change
@Override
public void onStateChanged(StateChangeEvent
stateChangeEvent) {
super.onStateChanged(stateChangeEvent);
// refresh the widget if the value on server
has changed
if
(stateChangeEvent.hasPropertyChanged("value")) {
getWidget().setValue(getState().value);
}
Код виджета RatingFieldWidget не определяет внешний
вид компонента, кроме назначения имён стилей
ключевым элементам. Для того, чтобы определить
внешний вид нашего компонента, создадим файлы
стилей. Для этого можно воспользоваться ссылкой CreateCreate
theme extensiontheme extension секции Project propertiesProject properties в навигаторе Studio. В
появившемся диалоге выбираем тему halo. Эта тема
использует вместо иконок глифы шрифта FontAwesome,
чем мы и воспользуемся. Studio создаст пустые файлы SCSS
для расширения темы в каталоге themes модуля webweb.
Стили каждого компонента принято выделять в
отдельный файл componentname.scss в каталоге
components/componentname в формате примеси SCSS. В
каталоге themes/halo модуля web создадим структуру
вложенных каталогов: components/ratingfield. Затем
внутри ratingfield создадим файл ratingfield.scss:
}
}
@mixin ratingfield($primary-stylename: ratingfield) {
Подключим этот файл в главном файле темы halo-
ext.scss:
@mixin ratingfield($primary-stylename: ratingfield) {
.#{$primary-stylename}-star {
font-family: FontAwesome;
font-size: $v-font-size--h2;
padding-right: round($v-unit-size/4);
cursor: pointer;
&:after {
content: '\f006'; // 'fa-star-o'
}
}
.#{$primary-stylename}-star-selected {
&:after {
content: '\f005'; // 'fa-star'
}
}
.#{$primary-stylename} .#{$primary-stylename}-
star:last-child {
padding-right: 0;
}
.#{$primary-stylename}.v-disabled .#{$primary-
stylename}-star {
cursor: default;
}
}
@import "../halo/halo";
@import "components/ratingfield/ratingfield";
Для демонстрации работы компонента создадим новый
экран в модуле webweb.
Назовите файл с экраном rating-screen.xml.
Добавим экран в меню приложения. Перейдите в секцию
Main menuMain menu навигатора Studio и нажмите кнопку EditEdit.
/* Define your theme modifications inside next mixin
*/
@mixin halo-ext {
@include halo;
@include ratingfield;
}
Main menuMain menu навигатора Studio и нажмите кнопку EditEdit.
Откроется редактор меню. Добавьте созданный экран в
меню application.
Перейдем к редактированию экрана rating-screen.xml в
IDE. Нам понадобится контейнер для нашего компонента,
объявим его в XML экрана:
Откроем класс контроллера экрана RatingScreen.java и
добавим код размещения нашего компонента на экране:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://caption"
class="com.company.ratingsample.web.screens.RatingScre
en"
messagesPack="com.company.ratingsample.web.screens">
<layout expand="container">
<vbox id="container">
<!-- we'll add vaadin component here-->
</vbox>
</layout>
</window>
package com.company.ratingsample.web.screens;
import
com.company.ratingsample.web.toolkit.ui.RatingFieldSer
verComponent;
import
com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.BoxLayout;
import
com.haulmont.cuba.web.gui.components.WebComponentsHelp
Запускаем сервер приложения и смотрим на результат.
4.5.5.5. 4.5.5.5. Поддержка собственных
компонентов в CUBA Studio CUBA Studio
com.haulmont.cuba.web.gui.components.WebComponentsHelp
er;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
import java.util.Map;
public class RatingScreen extends AbstractWindow {
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
super.init(params);
com.vaadin.ui.Layout containerLayout =
(Layout) WebComponentsHelper.unwrap(container);
RatingFieldServerComponent field = new
RatingFieldServerComponent();
field.setCaption("Rate this!");
containerLayout.addComponent(field);
}
}
В данном разделе описывается, как добавить поддержку
новых визуальных компонентов в дизайнер экранов CUBA
Studio. Новый компонент появится в палитре компонентов,
вы сможете перетаскивать его в рабочую область и
редактировать его свойства в панели свойств
компонента.
Рассмотрим процесс создания интеграции компонента
stepper, создание которого было описано в разделе
Подключение аддона Vaadin с интеграцией в Generic UI.
Откроем проект, содержащий компонент stepper.
Если указанный проект вы не создавали, то вы
можете воспроизвести шаги, описанные ниже, и на
новом проекте. Вы увидите поддержку компонента в
Studio, но не сможете запустить приложение.
Нажмем кнопку Extend StudioExtend Studio на панели Project propertiesProject properties.
Рассмотрим поля, которые необходимо заполнить.
Conguration nameConguration name - идентификатор конфигурации.
Введем значение stepper.
Component XML elementComponent XML element - имя компонента в том виде, как
он должен быть добавлен в XML-дескриптор экрана. В
нашем случае это stepper.
Поля Component class nameComponent class name и Component model class nameComponent model class name
будут заполнены автоматически на основе введенного
значения. Оставьте их значения без изменения.
Component namespace URIComponent namespace URI - пространство имен из XSD,
описывающего компонент. Если вы генерировали новый
компонент с помощью Studio, то узнать значение для
этого поля вы можете в файле ui-component.xsd.
Component namespace prexComponent namespace prex - префикс XML-элемента
компонента в XML-дескриптор экрана.
Standard propertiesStandard properties - стандартные свойства компонента,
которые должны быть доступны для редактирования в
панели свойств компонента в дизайнере экранов.
Выберите caption, datasource и property.
Свойства id, align, height, width, enable, stylename,
visible по умолчанию доступны для всех
компонентов.
Custom propertiesCustom properties - в данной таблице добавляются
специфичные для компонента свойства, которые
должны редактироваться в панели свойств дизайнера
должны редактироваться в панели свойств дизайнера
экранов.
Добавим следующие свойства:
manualInputmanualInput, тип Boolean, значение по умолчанию true
mouseWheelmouseWheel, тип Boolean, значение по умолчанию true
stepAmountstepAmount, тип Integer, значение по умолчанию 0
maxValuemaxValue, тип Integer, значение по умолчанию 0
minValueminValue, тип Integer, значение по умолчанию 0
Далее нажмитие кнопку OKOK.
Новые визуальные компоненты инициализируются при
старте сервера Studio. Откройте окно сервера Studio,
остановите сервер, выйдите из Studio, затем отройте и
запустите его снова.
Сгенерируем стандартные экраны для сущности Customer
заново, чтобы стереть результаты наших прошлых
экспериментов.
Переходим в секцию GENERIC UIGENERIC UI навигатора студии и
открываем экран customer-edit.
Для начала удалим поле score из fieldGroup, т.к. мы
хотим для его редактирования использовать
специальный компонент.
На палитре компонентов найдите новый компонент
Stepper и перетащите его на экран под fieldGroup.
Выделите компонент stepper и перейдите на закладку
свойств компонента PropertiesProperties.
Заполните необходимые поля:
idid - stepper
captioncaption - Stepper
datasourcedatasource - customerDs
propertyproperty - score
maxValuemaxValue - 50
После этого перейдите на закладку XMLXML, чтобы увидеть
После этого перейдите на закладку XMLXML, чтобы увидеть
результат.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.gui.customer.CustomerE
dit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.gui.customer"
xmlns:app="http://schemas.company.com/agd/0.1/ui-
component.xsd">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<app:stepper id="stepper"
caption="Stepper"
datasource="customerDs"
maxValue="50"
property="score"/>
<frame id="windowActions"
В XML экрана объявлено пространство имен компонента с
префиксом app, компонент stepper добавлен на экран, и у
него установлены необходимые свойства.
5. 5. Устройство платформы
Данная глава содержит подробное описание
архитектуры, компонентов и механизмов платформы.
5.1. 5.1. Архитектура
В данной главе рассмотрена архитектура CUBA-
приложений в различных разрезах: по уровням, блокам,
модулям и компонентам.
5.1.1. 5.1.1. Уровни и блоки приложения
Платформа позволяет строить приложения по
классической трехуровневой схеме: клиентский уровень,
средний слой, база данных.
Уровень
отражает степень
"удаленности" от хранимых данных.
В дальнейшем речь пойдет в основном о среднем слое и
клиентах, поэтому для краткости выражение "все уровни"
означает два этих уровня.
На каждом уровне возможно создание одного или
нескольких
блоков
(units) приложения. Блок представляет
собой обособленную исполняемую программу,
взаимодействующую с другими блоками приложения.
Средства платформы CUBA позволяют создавать блоки в
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
Средства платформы CUBA позволяют создавать блоки в
виде веб-приложений и десктопных приложений.
Рисунок 6. 6. Уровни и блоки приложения
MiddlewareMiddleware
Средний слой, содержащий основную бизнес-логику
приложения и выполняющий обращения к базе данных.
Представляет собой отдельное веб-приложение под
управлением стандартного контейнера Java EE Web
Prole. См. Компоненты среднего слоя.
Web ClientWeb Client
Основной блок клиентского уровня. Содержит
интерфейс, предназначенный, как правило, для
внутренних пользователей организации. Представляет
собой отдельное веб-приложение под управлением
стандартного контейнера Java EE Web Prole. Реализация
пользовательского интерфейса основана на фреймворке
VaadinVaadin. См. Универсальный пользовательский интерфейс.
Desktop ClientDesktop Client
Дополнительный блок клиентского уровня. Содержит
Дополнительный блок клиентского уровня. Содержит
интерфейс, предназначенный, как правило, для
внутренних пользователей организации. Представляет
собой десктопное Java-приложение, реализация
пользовательского интерфейса основана на фреймворке
Java SwingJava Swing. См. Универсальный пользовательский
интерфейс.
Web PortalWeb Portal
Дополнительный блок клиентского уровня. Может
содержать интерфейс для внешних пользователей и
средства интеграции с мобильными устройствами и
сторонними приложениями. Представляет собой
отдельное веб-приложение под управлением
стандартного контейнера Java EE Web Prole. Реализация
пользовательского интерфейса основана на фреймворке
Spring MVCSpring MVC. См. Компоненты портала.
Polymer ClientPolymer Client
Дополнительный клиентский блок на чистом JavaScript,
предоставляющий интерфейс для внешних
пользователей. Основан на фреймворке Google Polymer,
работает со средним слоем через REST API, запущенный в
блоке Web Client или Web Portal. См. Пользовательский
интерфейс на Polymer.
Обязательным блоком любого приложения является
средний слой - MiddlewareMiddleware. Для реализации
пользовательского интерфейса, как правило,
используется один или несколько клиентских блоков,
например, Web ClientWeb Client и Web PortalWeb Portal.
Все основанные на Java клиентские блоки
взаимодействуют со средним слоем одинаковым образом
посредством протокола HTTP, что позволяет размещать
средний слой произвольным образом, в том числе за
сетевым экраном. Следует отметить, что при
развертывании в простейшем случае среднего слоя и веб-
развертывании в простейшем случае среднего слоя и веб-
клиента на одном сервере между ними организуется
локальное взаимодействие в обход сетевого стека для
снижения накладных расходов.
5.1.2. 5.1.2. Модули приложения
Модуль – наименьшая структурная единица CUBA-
приложения. Представляет собой один модуль проекта
приложения и соответствующий ему JAR файл с
исполняемым кодом.
Стандартные модули:
globalglobal – включает в себя классы сущностей, интерфейсы
сервисов и другие общие для всех уровней классы.
Используется во всех блоках приложения.
corecore – реализация сервисов и всех остальных
компонентов среднего слоя. Используется только на
MiddlewareMiddleware.
guigui – общие компоненты Универсальный
пользовательский интерфейс. Используется в Web ClientWeb Client
и Desktop ClientDesktop Client.
webweb – реализация универсального пользовательского
интерфейса на VaadinVaadin, а также другие специфичные для
веб-клиента классы. Используется в блоке Web ClientWeb Client.
desktopdesktop – опциональный модуль – реализация
универсального пользовательского интерфейса на JavaJava
SwingSwing, а также другие специфичные для десктоп-
клиента классы. Используется в блоке Desktop ClientDesktop Client.
portalportal – опциональный модуль – реализация веб-портала
на Spring MVCSpring MVC.
polymer-clientpolymer-client – опциональный модуль – реализация
Пользовательский интерфейс на Polymer на JavaScript.
Рисунок 7. 7. Модули приложения
5.1.3. 5.1.3. Компоненты приложения
Функциональность платформы разделена на несколько
компонентов приложения
(ранее называемых
базовыми
проектами
):
cubacuba – основной компонент, содержит всю
функциональность, описанную в данном руководстве.
reportsreports – подсистема генерации отчетов.
ftsfts – подсистема полнотекстового поиска.
chartscharts – подсистема вывода диаграмм.
bpmbpm – механизм исполнения бизнес-процессов по
стандарту BPMN 2.0.
Проект приложения всегда зависит от одного или
нескольких компонентов. Это означает, что приложение
использует компоненты как библиотеки и включает их
функциональность.
Любое CUBA-приложение зависит от компонента cubacuba.
Остальные компоненты платформы являются
опциональными и могут быть включены в приложение при
необходимости. Все опциональные компоненты зависят
от cubacuba и могут также иметь зависимости между собой.
Ниже приведена диаграмма зависимостей между
компонентами платформы. Сплошными линиями
изображены обязательные зависимости, пунктирными −
опциональные.
опциональные.
Любое CUBA-приложение может в свою очередь быть
использовано как компонент другого приложения. Это
позволяет декомпозировать большие проекты в наборы
функциональных модулей, которые могут
разрабатываться независимо. Можно также создать в
организации набор собственных компонентов
использовать их в различных проектах, тем самым создав
собственную платформу более высокого уровня на основе
CUBA. Ниже приведена диаграмма возможной структуры
зависимостей компонентов приложения.
Для того чтобы приложение можно было использовать в
Для того чтобы приложение можно было использовать в
качестве компонента, оно должно содержать дескриптор
app-component.xml и специальный элемент в манифесте JAR
модуля globalglobal. CUBA Studio позволяет автоматически
сгенерировать дескриптор и манифест для текущего
проекта.
Практическое руководство по работе с собственным
компонентом приложения приведено в разделе
Использование компонентов приложения.
5.1.4. 5.1.4. Состав приложения
Вышеописанные архитектурные принципы напрямую
отражаются на составе собранного приложения.
Рассмотрим его на примере простого приложения salessales,
которое имеет два блока – MiddlewareMiddleware и Web ClientWeb Client; и
включает в себя функциональность компонентов cubacuba и
reportsreports.
Рисунок 8. 8. Состав простого приложения
На рисунке изображено содержимое некоторых
каталогов сервера TomcatTomcat с развернутым в нем
приложением salessales.
Блок MiddlewareMiddleware реализован веб-приложением app-core,
блок Web ClientWeb Client – веб-приложением app. Исполняемый код
веб-приложений содержится в каталогах WEB-INF/lib в
наборе JAR-файлов. Каждый JAR представляет собой
результат сборки (артефакт) одного из модулей
приложения или базового проекта.
Например, состав JAR-файлов веб-приложения среднего
слоя app-core определяется тем, что блок MiddlewareMiddleware
состоит из модулей globalglobal и corecore, и приложение использует
компоненты cubacuba и reportsreports.
5.2. 5.2. Общие компоненты
В данной главе рассмотрены компоненты платформы,
общие для всех уровней приложения.
5.2.1. 5.2.1. Модель данных
Предметная область моделируется в приложении с
помощью взаимосвязанных классов Java, называемых
классами сущностей или просто сущностями.
Сущности подразделяются на две категории:
Персистентные – экземпляры таких сущностей
хранятся в таблицах базы данных с помощью ORM.
Неперсистентные – экземпляры существуют только в
оперативной памяти, или сохраняются где-то с
помощью иных механизмов.
Сущности характеризуются своими атрибутами. Атрибут
соответствует полю класса и паре методов доступа (get /
соответствует полю класса и паре методов доступа (get /
set) к полю. Чтобы атрибут был неизменяемым (read only),
достаточно не создавать метод set.
Персистентные сущности могут включать в себя
атрибуты, не хранящиеся в БД. В случае
неперсистентного атрибута можно не создавать поле
класса, ограничившись методами доступа.
Класс сущности должен удовлетворять следующим
требованиям:
Наследоваться от одного из базовых классов,
предоставляемых платформой (см. ниже).
Мметь набор полей и методов доступа,
соответствующих атрибутам сущностей.
Класс и его поля (или методы доступа при отсутствии
для атрибута соответствующего поля) должны быть
определенным образом аннотированы для
предоставления нужной информации фреймворкам JPA
(в случае персистентной сущности) и метаданных.
Поддерживаются следующие типы атрибутов сущностей:
java.lang.String
java.lang.Boolean
java.lang.Integer
java.lang.Long
java.lang.Double
java.math.BigDecimal
java.util.Date
java.sql.Date
java.sql.Time
java.util.UUID
byte[]
enum
Cущность
Базовые классы сущностей (см. ниже) переопределяют
equals() и hashCode() таким образом, что экземпляры
сущностей сравниваются по их идентификаторам. То есть
экземпляры одного класса считаются равными, если
равны их идентификаторы.
5.2.1.1. 5.2.1.1. Базовые классы сущностей
Рассмотрим базовые классы и интерфейсы сущностей
более подробно.
Instance – декларирует базовые методы работы с
объектами предметной области:
Получение ссылки на мета-класс объекта.
Генерация имени экземпляра.
Чтение/установка значений атрибутов по имени.
Добавление слушателей, получающих уведомления об
Добавление слушателей, получающих уведомления об
изменениях атрибутов.
Entity – дополняет Instance понятием
идентификатора сущности, причем Entity не
определяет тип идентификатора, оставляя эту
возможность наследникам.
AbstractInstance – реализует логику работы со
слушателями изменения атрибутов.
AbstractInstance хранит слушателей в коллекции
WeakReference, т.е. при отсутствии внешних ссылок
на добавленного слушателя, он будет немедленно
уничтожен сборщиком мусора. Как правило,
слушателями изменения атрибутов являются
визуальные компоненты и источники данных UI, на
которые всегда имеются ссылки из других
объектов, поэтому проблема исчезновения
слушателей не возникает. Однако если слушатель
создается прикладным кодом и на него никто не
ссылается естественным образом, необходимо
кроме добавления в Instance сохранить его в
некотором поле объекта.
BaseGenericIdEntity - базовый класс персистентных и
неперсистентных сущностей. Реализует Entity, но не
специфицирует тип идентификатора (то есть
первичного ключа) сущности.
EmbeddableEntity - базовый класс персистентных
встраиваемых сущностей.
Ниже рассмотрены базовые классы, от которых
рекомендуется наследовать сущности. Неперсистентные
сущности наследуются от тех же классов, что и
персистентные. Фреймворк определяет, является ли
сущность персистентной или нет по файлу, в котором
зарегистрирован класс: persistence.xml или metadata.xml.
зарегистрирован класс: persistence.xml или metadata.xml.
StandardEntityStandardEntity
Наследуйте от StandardEntity, если необходим
стандартный набор свойств сущности: первичный ключ
типа UUID, экземпляры должны содержать информацию
о том, кто и когда создал и изменил их, необходима
оптимистичная блокировка и мягкое удаление.
HasUuid – интерфейс сущностей имеющих глобальные
уникальные идентификаторы
Versioned – интерфейс сущностей, поддерживающих
оптимистичную блокировку
Creatable – интерфейс сущностей, для которых
требуется сохранять информацию о том, кто и когда ее
создал
Updatable – интерфейс сущностей, для которых
требуется сохранять информацию о том, кто и когда
изменял экземпляр в последний раз
SoftDelete – интерфейс сущностей, поддерживающих
SoftDelete – интерфейс сущностей, поддерживающих
мягкое удаление
BaseUuidEntityBaseUuidEntity
Наследуйте от BaseUuidEntity если необходима
сущность с идентификатором типа UUID, но не нужны все
остальные свойства StandardEntity. Интерфейсы
Creatable, Versioned и др. можно выборочно
реализовать в конкретном классе сущности.
BaseLongIdEntityBaseLongIdEntity
Наследуйте от BaseLongIdEntity или
BaseIntegerIdEntity если необходима сущность с
идентификатором типа Long или Integer. Интерфейсы
Creatable, Versioned и др. можно выборочно
реализовать в конкретном классе сущности.
Рекомендуется реализовать HasUuid, так как это
позволяет платформе в некоторых случаях работать с
сущностью более оптимально, а кроме того, сущность
получает уникальный идентификатор в распределенном
окружении.
BaseStringIdEntityBaseStringIdEntity
Наследуйте от BaseStringIdEntity если необходима
сущность с идентификатором типа String. Интерфейсы
Creatable, Versioned и др. можно выборочно
реализовать в конкретном классе сущности.
Рекомендуется реализовать HasUuid, так как это
позволяет платформе в некоторых случаях работать с
сущностью более оптимально, а кроме того, сущность
получает уникальный идентификатор в распределенном
окружении. В конкретном классе сущности,
унаследованной от BaseStringIdEntity, необходимо
задать атрибут-идентификатор типа String и добавить
ему JPA-аннотацию @Id.
BaseIdentityIdEntityBaseIdentityIdEntity
Наследуйте от BaseIdentityIdEntity если необходимо
отобразить сущность на таблицу с первичным ключом
типа IDENTITY. Интерфейсы Creatable, Versioned и др.
можно выборочно реализовать в конкретном классе
сущности. Рекомендуется реализовать HasUuid, так как
это позволяет платформе в некоторых случаях работать
это позволяет платформе в некоторых случаях работать
с сущностью более оптимально, а кроме того, сущность
получает уникальный идентификатор в распределенном
окружении. Атрибут id сущности (т.е. методы getId/setId)
будут иметь тип IdProxy, который предназначен для
использования вместо реального идентификатора, пока
он не сгенерирован базой данных на вставке записи.
BaseGenericIdEntityBaseGenericIdEntity
Наследуйте напрямую от BaseGenericIdEntity если
необходимо отобразить сущность на таблицу с
композитным первичным ключом. В этом случае в классе
сущности необходимо создать поле встраиваемого типа,
представляющего композитный ключ, и аннотировать
его JPA-аннотацией @EmbeddedId.
5.2.1.2. 5.2.1.2. Аннотации сущностей
В данном разделе описаны все поддерживаемые
платформой аннотации классов и атрибутов сущностей.
Аннотации пакета javax.persistence обеспечивают
работу JPA, аннотации пакетов com.haulmont.*
работу JPA, аннотации пакетов com.haulmont.*
предназначены для управления метаданными и другими
механизмами платформы.
Если для аннотации указано только простое имя класса,
подразумевается что это класс платформы,
расположенный в одном из пакетов com.haulmont.*
5.2.1.2.1. 5.2.1.2.1. Аннотации класса
@Embeddable@Embeddable
See javax.persistence.Embeddable.
Определяет встраиваемую сущность, экземпляры
которой хранятся вместе с владеющей сущностью в той
же таблице.
Для задания имени сущности требуется применение
аннотации @MetaClass.
@EnableRestore@EnableRestore
Указывает, что экземпляры данной сущности доступны
для восстановления после мягкого удаления в
специальном экране core$Entity.restore, доступном
через пункт Administration > Data RecoveryAdministration > Data Recovery главного меню.
@Entity@Entity
См. javax.persistence.Entity.
Объявляет класс сущностью модели данных.
Параметры:
name - имя сущности, обязательно должно начинаться с
префикса, отделенного знаком $. Желательно
использовать в качестве префикса короткое имя
проекта для формирования отдельного пространства
имен.
Пример:
@Entity(name = "sales$Customer")
@Extends@Extends
Указывает, что данная сущность является расширением
и должна повсеместно использоваться вместо базовой.
См. Расширение функциональности.
@DiscriminatorColumn@DiscriminatorColumn
См. javax.persistence.DiscriminatorColumn.
Используется для определения колонки БД, отвечающей
за различение типов сущностей в случае стратегий
наследования SINGLE_TABLE и JOINED.
Параметры:
name - имя колонки-дискриминатора
discriminatorType - тип данных колонки-
дискриминатора
Пример:
@DiscriminatorValue@DiscriminatorValue
См. javax.persistence.DiscriminatorValue.
Определяет значение колонки-дискриминатора для
данной сущности. Эта аннотация должна быть
помещена на конкретном классе сущности.
Пример:
@Inheritance@Inheritance
См. javax.persistence.Inheritance.
@Entity(name = "sales$Customer")
@DiscriminatorColumn(name = "TYPE",
discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue("0")
Определяет стратегию наследования для иерархии
классов сущностей. Данная аннотация должна быть
помещена на корневом классе иерархии.
Параметры:
strategy - стратегия, по умолчанию SINGLE_TABLE
@Listeners@Listeners
Определяет список слушателей, предназначенных для
реакции на события жизненного цикла экземпляров
сущности на уровне Middleware.
Значением аннотации должна быть строка или массив
строк с именами бинов слушателей - см. Entity Listeners.
Примеры:
@MappedSuperclass@MappedSuperclass
См. javax.persistence.MappedSuperclass.
Определяет, что данный класс является предком
некоторых сущностей, и его атрибуты должны быть
использованы в составе сущностей-наследников. Такой
класс не сопоставляется никакой отдельной таблице
БД.
@MetaClass@MetaClass
Используется для объявления неперсистентной или
встраиваемой сущности (т.е. когда аннотация
@javax.persistence.Entity не применима)
Параметры:
@Listeners("sample_UserEntityListener")
@Listeners({"sample_FooListener","sample_BarListener
"})
name - имя сущности, обязательно должно начинаться с
префикса, отделенного знаком $. Желательно
использовать в качестве префикса короткое имя
проекта для формирования отдельного пространства
имен.
Пример:
@NamePattern@NamePattern
Определяет способ получения имени экземпляра,
возвращаемого методом Instance.getInstanceName().
Значением аннотации должна быть строка вида {0}|{1},
где
{0} - строка форматирования по правилам
String.format(), или имя метода данного объекта с
префиксом #. Метод должен возвращать String и не
иметь параметров.
{1} - разделенный запятыми список имен полей класса,
соответствующий формату {0}. В случае
использования в {0} метода список полей все равно
необходим, так как по нему формируется
представление _minimal.
Примеры:
@PostConstruct@PostConstruct
Данная аннотация может быть указана для метода
@MetaClass(name = "sales$Customer")
@NamePattern("%s|name")
@NamePattern("#getCaption|login,name")
Данная аннотация может быть указана для метода
класса. Такой метод будет вызван сразу после создания
экземпляра сущности через Metadata.create(). Это удобно,
если для инициализации экземпляра сущности
требуется вызов каких-либо бинов. Пример см. в
Инициализация полей сущности.
@PrimaryKeyJoinColumn@PrimaryKeyJoinColumn
См. javax.persistence.PrimaryKeyJoinColumn.
Используется в случае стратегии наследования JOINED
для указания колонки внешнего ключа данной сущности,
ссылающегося на первичный ключ сущности-предка.
Параметры:
name - имя колонки внешнего ключа данной сущности
referencedColumnName - имя колонки первичного
ключа сущности предка
Пример:
@SystemLevel@SystemLevel
Указывает, что данная сущность является системной и
не должна быть доступна для выбора пользователем в
различных списках сущностей, например, как тип
параметра универсального фильтра или тип
динамического атрибута.
@Table@Table
См. javax.persistence.Table.
Определяет таблицу базы данных для данной сущности.
Параметры:
name - имя таблицы
Пример:
@PrimaryKeyJoinColumn(name = "CARD_ID",
referencedColumnName = "ID")
Пример:
@TrackEditScreenHistory@TrackEditScreenHistory
Указывает, что для данной сущности будет
запоминаться история открытия экранов
редактирования ({имя_сущности}.edit) с
возможностью отображения в специальном экране
sec$ScreenHistory.browse, доступном через пункт Help >Help >
HistoryHistory главного меню.
5.2.1.2.2. 5.2.1.2.2. Аннотации атрибутов
Аннотации атрибутов устанавливаются на
соответствующие поля класса, за одним исключением:
если требуется объявить неизменяемый (read only)
неперсистентный атрибут foo, то достаточно создать
метод доступа getFoo() и поместить на этот метод
аннотацию @MetaProperty.
@CaseConversion@CaseConversion
Применяет автоматическую конвертацию регистра к
текстовым полям ввода, связанным с аннотированным
атрибутом.
Параметры:
type - тип конвертации: UPPER (по умолчанию), LOWER.
Пример:
@Column@Column
См. javax.persistence.Column.
@Table(name = "SALES_CUSTOMER")
@CaseConversion(type = ConversionType.UPPER)
@Column(name = "COUNTRY_CODE")
protected String countryCode;
См. javax.persistence.Column.
Определяет колонку БД, в которой будут храниться
значения данного атрибута.
Параметры:
name - имя колонки
length - (необязательный параметр, по умолчанию 255)
- длина колонки. Используется также при
формировании метаданных и, в конечном счете, может
ограничивать максимальную длину вводимого текста
в визуальных компонентах, работающих с данным
атрибутом. Для отмены ограничения по длине
атрибуту необходимо добавить аннотацию @Lob.
nullable - (необязательный параметр, по умолчанию
true) - может ли атрибут содержать null. При
указании nullable = false JPA контролирует наличие
значения поля при сохранении, кроме того, визуальные
компоненты, работающие с данным атрибутом, могут
сигнализировать пользователю о необходимости
ввода значения.
@Composition@Composition
Указывает на то, что связь является композицией - более
тесным вариантом ассоциации. Это означает, что
связанная сущность имеет смысл только как часть
владеющей сущности, т.е. создается и удаляется вместе
с ней.
Например, список пунктов в заказе (класс Order
содержит коллекцию экземпляров Item):
Другой пример - one-to-one отношение:
@OneToMany(mappedBy = "order")
@Composition
protected List<Item> items;
Другой пример - one-to-one отношение:
Указание для связи аннотации @Composition позволяет
организовать в экранах редактирования специальный
режим коммита источников данных, при котором
изменения экземпляров детализирующей сущности
сохраняются в базе данных только при коммите
основной сущности. Подробнее см. Редактирование
композитных сущностей.
@Embedded@Embedded
См. javax.persistence.Embedded.
Определяет атрибут типа встраиваемой сущности, в
свою очередь аннотированной @Embeddable.
Пример:
@EmbeddedParameters@EmbeddedParameters
По умолчанию ORM не создает экземпляр встроенной
сущности если все ее атрибуты равны null в базе данных.
Аннотацию @EmbeddedParameters можно использовать
для указания того, что экземпляр всегда должен
создаваться, например:
@Composition
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DETAILS_ID")
protected CustomerDetails details;
@Embedded
protected Address address;
@Embedded
@EmbeddedParameters(nullAllowed = false)
protected Address address;
@Id@Id
См. javax.persistence.Id.
Указывает, что данный атрибут является первичным
ключом сущности. Обычно эта аннотация присутствует
на поле базового класса, такого как BaseUuidEntity.
Использовать эту аннотацию в конкретном классе
сущности необходимо только при наследовании от
базового класса BaseStringIdEntity (то есть при
создании сущности со строковым первичным ключом).
@IgnoreUserTimeZone@IgnoreUserTimeZone
Для атрибутов типа timestamp с аннотацией
@javax.persistence.Temporal.TIMESTAMP заставляет
платформу игнорировать часовой пояс пользователя,
если он задан для текущей сессии.
@JoinColumn@JoinColumn
См. javax.persistence.JoinColumn.
Используется для указания колонки БД, определяющей
ассоциацию между сущностями. Наличие этой
аннотации указывает, что данная сторона отношения
является владеющей (owning).
Параметры:
name - имя колонки
Пример:
@JoinTable@JoinTable
См. javax.persistence.JoinTable.
protected Address address;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
См. javax.persistence.JoinTable.
Используется для указания таблицы связи на ведущей
стороне @ManyToMany ассоциации.
Параметры:
name - имя таблицы связи
joinColumns - элемент @JoinColumn, определяющий
колонку таблицы связей, соответствующую
первичному ключу ведущей стороны ассоциации (т.е.
содержащей аннотацию @JoinTable)
inverseJoinColumns - элемент @JoinColumn,
определяющий колонку таблицы связей,
соответствующую первичному ключу ведомой стороны
ассоциации
Пример атрибута customers класса Group, являющегося
ведущей стороной ассоциации:
Пример атрибута groups класса Customer, являющегося
ведомой стороной этой же ассоциации:
@Lob@Lob
См. javax.persistence.Lob.
Указывает, что данный атрибут не имеет ограничений
@ManyToMany
@JoinTable(name = "SALES_CUSTOMER_GROUP_LINK",
joinColumns = @JoinColumn(name = "GROUP_ID"),
inverseJoinColumns = @JoinColumn(name =
"CUSTOMER_ID"))
protected Set<Customer> customers;
@ManyToMany(mappedBy = "customers")
protected Set<Group> groups;
длины. Применяется совместно с аннотацией @Column.
Если @Lob указан, то длина, заданная в @Column явно или
по умолчанию, игнорируется.
Пример:
@LocalizedValue@LocalizedValue
Служит для описания способа получения
локализованного значения некоторого изменяющегося
атрибута, которое возвращает метод
MessageTools.getLocValue().
Параметры:
messagePack - явное указание пакета, из которого
будет взято локализованное сообщение, например,
com.haulmont.cuba.core.entity
messagePackExpr - выражение в терминах пути к
атрибуту, хранящему имя пакета, из которого будет
взято локализованное сообщение, например
proc.messagesPack. Путь начинается с атрибута
текущей сущности.
Пример аннотации, означающей, что локализованное
значение атрибута state будет взято из пакета, имя
которого хранится в атрибуте messagesPack связанной
сущности proc:
@Column(name = "DESCRIPTION")
@Lob
private String description;
@Column(name = "STATE")
@LocalizedValue(messagePackExpr =
"proc.messagesPack")
protected String state;
@Lookup@Lookup
Определяет тип просмотра ссылочных атрибутов.
Параметры:
type - по умолчанию имеет значение SCREEN, при
котором ссылки открываются через lookup-экран.
Значение DROPDOWN позволяет открывать ссылки в
виде выпадающего списка. Если за способ
отображения выбран DROPDOWN, Studio создаст options
datasource для выпадающего списка при скаффолдинге
экрана редактирования. Таким образом, параметр
Lookup type необходимо задать ДО генерации экрана
редактирования сущности. Кроме того, компонент
Filter позволит пользователям выбирать параметры
фильтрации также из выпадающего списка вместо
lookup-экрана.
actions - определяет действия, которые будут
использованы в компоненте PickerField в составе
FieldGroup по умолчанию. Возможные значения:
lookup, clear, open.
@ManyToMany@ManyToMany
См. javax.persistence.ManyToMany.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROC_ID")
protected Proc proc;
@Lookup(type = LookupType.DROPDOWN, actions =
{"open"})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
Определяет атрибут-коллекцию ссылок на сущность с
типом ассоциации много-ко-многим.
Ассоциация много-ко-многим всегда имеет ведущую
сторону и может иметь обратную сторону - ведомую. На
ведущей стороне указывается дополнительная
аннотация @JoinTable, на ведомой стороне - параметр
mappedBy.
Параметры:
mappedBy - поле связанной сущности, определяющее
ассоциацию с ведущей стороны. Необходимо
указывать только на ведомой стороне.
targetEntity - тип связанной сущности.
Необязательный параметр, если коллекция объявлена
с использованием Java genericsJava generics.
fetch - (необязательный параметр, по умолчанию LAZY)
- определяет, будет ли JPA жадно загружать
коллекцию связанных сущностей. Необходимо всегда
оставлять значение по умолчанию LAZY, так как в CUBA-
приложении политика загрузки связей определяется
динамически на основе механизма представлений.
@ManyToOne@ManyToOne
См. javax.persistence.ManyToOne.
Определяет атрибут-ссылку на сущность с типом
ассоциации много-к-одному.
Параметры:
fetch - (по умолчанию EAGER) параметр,
определяющий, будет ли JPA жадно загружать
ассоциированную сущность. Данный параметр всегда
должен быть установлен в значение LAZY, так как в
CUBA-приложении политика загрузки связей
определяется динамически на основе механизма
представлений.
представлений.
optional - (необязательный параметр, по умолчанию
true) - может ли атрибут содержать null. При
указании optional = false JPA контролирует наличие
ссылки при сохранении, кроме того, визуальные
компоненты, работающие с данным атрибутом, могут
сигнализировать пользователю о необходимости
ввода значения.
Например, несколько экземпляров Order (заказов)
ссылаются на один экземпляр Customer (покупателя), в
этом случае класс Order должен содержать следующее
объявление:
@MetaProperty@MetaProperty
Указывает, что данный атрибут должен быть включен в
метаданные. Данная аннотация может быть
установлена как на поле класса, так и на метод доступа,
в случае отсутствия соответствующего атрибуту поля.
Данная аннотация не обязательна для полей,
снабженных следующими аннотациями пакета
javax.persistence: @Column, @OneToOne, @OneToMany,
@ManyToOne, @ManyToMany, @Embedded. Такие поля
отражаются в метаданных автоматически. Поэтому
@MetaProperty в основном применяется для
определения неперсистентных атрибутов сущностей.
Параметры (опционально):
mandatory - может ли атрибут содержать null. При
указании mandatory = true визуальные компоненты,
работающие с данным атрибутом, могут
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
работающие с данным атрибутом, могут
сигнализировать пользователю о необходимости
ввода значения.
datatype - явно задает datatype, чтобы переопределить
datatype задаваемый Java-типом атрибута.
related - задает массив связанных персистентных
атрибутов, которые должны быть загружены из БД,
если данный атрибут включен во view.
Пример использования для поля:
Пример использования для метода:
@NumberFormat@NumberFormat
Задает формат атрибута типа Number (это может быть
BigDecimal, Integer, Long или Double). Значения такого
атрибута будут форматироваться в пользовательском
интерфейсе в соответствии с указанными параметрами
аннотации:
pattern - паттерн форматирования, задается по
@Transient
@MetaProperty
protected String token;
@MetaProperty
public String getLocValue() {
if (!StringUtils.isBlank(messagesPack)) {
return
AppBeans.get(Messsages.class).getMessage(messagesPack
, value);
} else {
return value;
}
}
pattern - паттерн форматирования, задается по
правилам, описанным в DecimalFormat.
decimalSeparator - символ, используемый в качестве
разделителя целой и дробной части (опционально).
groupingSeparator - символ, используемый в качестве
разделителя групп разрядов (optional).
Если decimalSeparator и/или groupingSeparator не
указаны, фреймворк использует соответствующие
значения из format strings для локали текущего
пользователя. При форматировании без учета локали в
этом случае используются символы из системной локали
сервера.
Примеры:
@OnDelete@OnDelete
@Column(name = "PRECISE_NUMBER", precision = 19,
scale = 4)
@NumberFormat(pattern = "0.0000")
protected BigDecimal preciseNumber;
@Column(name = "WEIRD_NUMBER", precision = 19, scale
= 4)
@NumberFormat(pattern = "#,##0.0000",
decimalSeparator = "_", groupingSeparator = "`")
protected BigDecimal weirdNumber;
@Column(name = "SIMPLE_NUMBER")
@NumberFormat(pattern = "#")
protected Integer simpleNumber;
@Column(name = "PERCENT_NUMBER", precision = 19,
scale = 4)
@NumberFormat(pattern = "#%")
protected BigDecimal percentNumber;
@OnDelete@OnDelete
Определяет политику обработки связи в случае мягкого
удаления сущности, содержащей данный атрибут. См.
Мягкое удаление.
Пример:
@OnDeleteInverse@OnDeleteInverse
Определяет политику обработки связи в случае мягкого
удаления сущности с обратной стороны ассоциации. См.
Мягкое удаление.
Пример:
@OneToMany@OneToMany
См. javax.persistence.OneToMany.
Определяет атрибут-коллекцию ссылок на сущность с
типом ассоциации один-ко-многим.
Параметры:
mappedBy - поле связанной сущности, определяющее
ассоциацию
targetEntity - тип связанной сущности.
Необязательный параметр, если коллекция объявлена
с использованием Java genericsJava generics.
fetch - (необязательный параметр, по умолчанию LAZY)
@OneToMany(mappedBy = "group")
@OnDelete(DeletePolicy.CASCADE)
private Set<Constraint> constraints;
@ManyToOne
@JoinColumn(name = "DRIVER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
private Driver driver;
fetch - (необязательный параметр, по умолчанию LAZY)
- определяет, будет ли JPA жадно загружать
коллекцию связанных сущностей. Необходимо всегда
оставлять значение по умолчанию LAZY, так как в CUBA-
приложении политика загрузки связей определяется
динамически на основе механизма представлений.
cascade - (необязательный параметр, по умолчанию {})
- каскадирование операций определяет, какие
операции над сущностью должны быть применены к
ассоциированным сущностям. Каскадирование на
данном уровне не рекомендуется использовать.
Например, несколько экземпляров Item (пунктов заказа)
ссылаются на один экземпляр Order (заказ) с помощью
@ManyToOne поля Item.order, в этом случае класс Order
может содержать коллекцию экземпляров Item:
@OneToOne@OneToOne
См. javax.persistence.OneToOne.
Определяет атрибут-ссылку на сущность с типом
ассоциации один-к-одному.
Параметры:
fetch - (по умолчанию EAGER) параметр,
определяющий, будет ли JPA жадно загружать
ассоциированную сущность. Данный параметр всегда
должен быть установлен в значение LAZY, так как в
CUBA-приложении политика загрузки связей
определяется динамически на основе механизма
представлений.
mappedBy - поле связанной сущности, определяющее
ассоциацию. Требуется устанавливать только на
@OneToMany(mappedBy = "order")
protected Set<Item> items;
ассоциацию. Требуется устанавливать только на
ведомой стороне ассоциации.
optional - (необязательный параметр, по умолчанию
true) - может ли атрибут содержать null. При
указании optional = false JPA контролирует наличие
ссылки при сохранении, кроме того, визуальные
компоненты, работающих с данным атрибутом, могут
сигнализировать пользователю о необходимости
ввода значения.
Пример ведущей стороны ассоциации, класс Driver:
Пример ведомой стороны ассоциации, класс
DriverCallsign:
@OrderBy@OrderBy
См. javax.persistence.OrderBy.
Определяет порядок элементов в атрибуте-коллекции
на момент извлечения из базы данных. Данную
аннотацию необходимо задавать для упорядоченных
коллекций, таких как List или LinkedHashSet для
получения предсказуемого порядка следования
элементов.
Параметры:
value - строка, определяющая порядок, в формате:
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CALLSIGN_ID")
protected DriverCallsign callsign;
@OneToOne(fetch = FetchType.LAZY, mappedBy =
"callsign")
protected Driver driver;
Пример:
@Temporal@Temporal
См. javax.persistence.Temporal.
Для атрибута типа java.util.Date уточняет тип
хранимого значения: дата, время или дата+время.
Параметры:
value - тип хранимого значения: DATE, TIME, TIMESTAMP
Пример:
@Transient@Transient
См. javax.persistence.Transient.
Указывает, что данное поле не хранится в БД, т.е.
является неперсистентным.
Поля поддерживаемых JPA типов (см.
http://docs.oracle.com/javaee/7/api/javax/persistence/Basic.html)
по умолчанию являются персистентными
, поэтому
аннотация @Transient обязательна для объявления
неперсистентного атрибута такого типа.
orderby_list::= orderby_item [,orderby_item]*
orderby_item::= property_or_field_name [ASC | DESC]
@OneToMany(mappedBy = "user")
@OrderBy("createTs")
protected List<UserRole> userRoles;
@Column(name = "START_DATE")
@Temporal(TemporalType.DATE)
protected Date startDate;
Для включения @Transient атрибута в метаданные,
необходимо также указать аннотацию @MetaProperty.
@Version@Version
См. javax.persistence.Version.
Указывает, что данное поле хранит версию для
поддержки оптимистичной блокировки сущностей.
Применение такого поля необходимо при реализации
классом сущности интерфейса Versioned (базовый класс
StandardEntity уже содержит такое поле).
Пример:
5.2.1.3. 5.2.1.3. Атрибуты типа enum enum
В стандартном варианте использования JPA для
атрибутов типа enum в базе данных хранится целое число,
получаемое методом ordinal() этого перечисления.
Такой подход может привести к следующим проблемам
при эксплуатации и развитии системы:
при появлении в БД значения, не равного ни одному
ordinal значению перечисления, экземпляр сущности
нельзя загрузить вообще;
невозможно ввести новое значение между
имеющимися, что актуально при использовании
сортировки по значению перечисления (order by).
Чтобы решить эти проблемы, в подходе CUBA
предлагается отвязать значение, хранимое в БД, от
ordinal перечисления. Для этого необходимо поле класса
@Version
@Column(name = "VERSION")
private Integer version;
ordinal перечисления. Для этого необходимо поле класса
сущности объявлять с типом, хранимым в БД (Integer или
String), а методы доступа (getter / setter) создавать для
типа самого перечисления.
Например:
При этом сам класс перечисления может выглядеть
следующим образом:
@Entity(name = "sales$Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {
@Column(name = "GRADE")
protected Integer grade;
public CustomerGrade getGrade() {
return grade == null ? null :
CustomerGrade.fromId(grade);
}
public void setGrade(CustomerGrade grade) {
this.grade = grade == null ? null :
grade.getId();
}
...
}
public enum CustomerGrade implements
EnumClass<Integer> {
PREMIUM(10),
HIGH(20),
MEDIUM(30);
Для правильного отражения в метаданных класс
перечисления, используемый в качестве типа атрибута
сущности, должен реализовывать интерфейс EnumClass.
Как видно из примеров, для атрибута grade в БД хранится
значение типа Integer, задаваемое полем id
перечисления CustomerGrade, а конкретно 10, 20 или 30. В
то же время прикладной код и метаданные работают с
самим типом CustomerGrade через методы доступа,
которые и осуществляют конвертацию.
При наличии в поле БД значения, не соответствующего ни
одному значению перечисления, метод getGrade() просто
вернет null. Для ввода нового значения, например,
HIGHER, между HIGH и PREMIUM, достаточно добавить это
значение в перечисление с идентификатором 15, при этом
private Integer id;
CustomerGrade(Integer id) {
this.id = id;
}
@Override
public Integer getId() {
return id;
}
public static CustomerGrade fromId(Integer id) {
for (CustomerGrade grade :
CustomerGrade.values()) {
if (grade.getId().equals(id))
return grade;
}
return null;
}
}
значение в перечисление с идентификатором 15, при этом
сортировка по полю Customer.grade останется верной.
Тип поля Integer удобно использовать в случаях, когда
необходим упорядоченный список констант, подлежащий
сортировке, например, в запросах JPQL и SQL (>, <, >=, ⇐,
order by), кроме того, он имеет незначительное
преимущество перед String в плане производительности
формата хранения и занимаемого места. С другой
стороны, значения типа Integer сами по себе неочевидны
и могут затруднять отладку и интерпретацию
результатов запросов, они неудобны в работе с голыми
данными и сериализованными форматами. Если
отношение упорядочения между константами не
требуется, удобнее использовать тип String.
Перечисления могут быть созданы в CUBA Studio на
вкладке DATA MODELDATA MODEL (New → Enumeration). Чтобы
использовать перечисление в качестве атрибута
сущности, в редакторе атрибута нужно выбрать ENUM в
поле Attribute typeAttribute type и класс перечисления в поле TypeType.
Значениям перечисления могут быть сопоставлены
локализованные названия для отображения в
пользовательском интерфейсе приложения.
5.2.1.4. 5.2.1.4. Мягкое удаление
Платформа CUBA поддерживает режим "мягкого
удаления" данных - когда вместо удаления записей из
базы данных они только помечаются определенным
образом и становятся недоступными для обычного
использования. В дальнейшем такие записи можно либо
совсем удалить из БД с помощью отдельной
регламентной процедуры, либо восстановить.
Механизм мягкого удаления является "прозрачным" для
прикладного программиста - достаточно убедиться, что
класс сущности реализует интерфейс SoftDelete, и
платформа сама нужным образом будет модифицировать
запросы и операции с данными.
Режим мягкого удаления имеет следующие
преимущества:
значительно снижается риск потери данных вследствие
неверных действий пользователей
позволяет быстро сделать некоторые записи
недоступными, даже если на них имеются ссылки.
Возьмем для примера модель данных Заказы -
Покупатели. Допустим, на некоторого покупателя
оформлено несколько заказов, однако нам нужно
сделать его недоступным для дальнейшей работы
пользователей. Традиционным "жестким" удалением
сделать это невозможно, так как для удаления
покупателя нам нужно либо удалить все его заказы,
либо обнулить в этих заказах ссылки на него (т.е.
потерять информацию). При мягком удалении
покупателя он становится недоступным для поиска и
изменения, однако при просмотре заказов пользователь
видит на экране имя покупателя, так как при загрузке
связей признак удаления намеренно игнорируется.
Описанное поведение является стандартным, но может
быть модифицировано с помощью политики обработки
связей при удалении.
Восстановить удалённые сущности можно через экран
Restore Deleted EntitiesRestore Deleted Entities, по умолчанию доступный в
стандартном меню AdministrationAdministration приложения. Эта
функциональность предназначена для использования
администраторами системы, имеющими разрешения на
все сущности. Её следует использовать с осторожностью,
также рекомендуется ограничить доступ к этому экрану
также рекомендуется ограничить доступ к этому экрану
для простых пользователей системы.
Отрицательной стороной мягкого удаления является
увеличение объема базы данных и потенциальная
необходимость дополнительных процедур ее очистки.
5.2.1.4.1. 5.2.1.4.1. Использование
Для того чтобы экземпляры сущности удалялись мягко,
класс сущности должен реализовывать интерфейс
SoftDelete, а соответствующая таблица БД должна
содержать колонки:
DELETE_TS - когда удалена запись
DELETED_BY - логин пользователя, который удалил
запись
Поведение системы по умолчанию - сущности,
реализующие SoftDelete, удаляются мягко, удаленные
сущности не возвращаются запросами и поиском по
идентификатору. При необходимости такое поведение
можно динамически отключить следующими способами:
для текущего экземпляра EntityManager - вызовом
setSoftDeletion(false)
при запросе данных через DataManager - вызовом у
передаваемого объекта LoadContext метода
setSoftDeletion(false)
на уровне источников данных - используя метод
CollectionDatasource.setSoftDeletion(false) или
атрибут softDeletion="false" элемента
collectionDatasource в XML-дескрипторе экрана.
В режиме мягкого удаления платформа автоматически
отфильтровывает удаленные экземпляры при загрузке по
идентификатору и по JPQL-запросу, а также удаленные
элементы связанных сущностей в атрибутах-коллекциях.
Однако связанные сущности в единичных (*ToOne)
Однако связанные сущности в единичных (*ToOne)
атрибутах загружаются независимо от того, удален
связанный экземпляр или нет.
5.2.1.4.2. 5.2.1.4.2. Политика обработки связей
Платформа предоставляет средство обработки связей
при удалении сущностей, во многом аналогичное
правилам ON DELETEON DELETE внешних ключей в базе данных. Это
средство работает на уровне Middleware и использует
аннотации @OnDelete, @OnDeleteInverse атрибутов сущности.
Аннотация @OnDelete обрабатывается при удалении той
сущности, в которой она встретилась, а не той, на которую
указывает аннотированный атрибут (в этом отличие от
каскадных удалений на уровне БД).
Аннотация @OnDeleteInverse обрабатывается при
удалении той сущности, на которую указывает
аннотированный атрибут, (т.е. аналогично каскадному
удалению на уровне внешних ключей в БД). Эта
аннотация полезна при отсутствии в удаляемом объекте
атрибута, который нужно проверять при удалении. При
этом, как правило, в проверяемом объекте существует
ссылка на удаляемый, на этот атрибут и устанавливается
аннотация @OnDeleteInverse.
Значением аннотации может быть:
DeletePolicy.DENY - запретить удаление сущности, если
аннотированный атрибут не null или не пустая
коллекция
DeletePolicy.CASCADE - каскадно удалить
аннотированный атрибут
DeletePolicy.UNLINK - разорвать связь с
аннотированным атрибутом. Разрыв связи имеет смысл
указывать только на ведущей стороне ассоциации - той,
которая в классе сущности аннотирована @JoinColumn.
Примеры:
1. Запрет удаления при наличии ссылки: при попытке
удаления экземпляра Customer, на который ссылается
хотя бы один Order, будет выброшено исключение
DeletePolicyException.
Order.java
Customer.java
Сообщения в окне исключения могут быть
локализованы в главном пакете сообщений.
Используйте для этого следующие ключи:
deletePolicy.caption - заголовок уведомления.
deletePolicy.references.message - тело сообщения.
deletePolicy.caption.sales$Customer - заголовок
уведомления для конкретной сущности.
deletePolicy.references.message.sales$Customer -
тело сообщения для конкретной сущности.
2. Каскадное удаление элементов коллекции: при
удалении экземпляра Role все экземпляры Permission
также будут удалены.
Role.java
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
protected Customer customer;
@OneToMany(mappedBy = "customer")
protected List<Order> orders;
@OneToMany(mappedBy = "role")
Permission.java
3. Разрыв связи с элементами коллекции: удаление
экземпляра Role приведет к установке в null ссылок со
стороны всех входивших в коллекцию экземпляров
Permission.
Role.java
Permission.java
Особенности реализации:
1. Политика обработки связей реализуется при
сохранении данных в БД на уровне Middleware.
2. Нужно быть осторожным при использовании
@OnDeleteInverse с политиками CASCADE и UNLINK, так
как при этом происходит извлечение из БД на сервер
приложения всех экземпляров ссылающихся объектов,
@OneToMany(mappedBy = "role")
@OnDelete(DeletePolicy.CASCADE)
protected Set<Permission> permissions;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ROLE_ID")
protected Role role;
@OneToMany(mappedBy = "role")
protected Set<Permission> permissions;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ROLE_ID")
@OnDeleteInverse(DeletePolicy.UNLINK)
protected Role role;
приложения всех экземпляров ссылающихся объектов,
изменение и затем сохранение.
Например, в случае ассоциации Customer - Job и
большого количества работ для одного заказчика, если
поставить на атрибут Job.customer политику
@OnDeleteInverse(CASCADE), то при удалении
экземпляра заказчика будет предпринята попытка
извлечь и изменить все его работы. Это может привести
к перегрузке сервера приложения и БД.
С другой стороны, использование
@OnDeleteInverse(DENY) безопасно, так как при этом
производится только подсчет количества ссылающихся
объектов, и если оно больше 0, выбрасывается
исключение. Поэтому @OnDeleteInverse(DENY) для
атрибута Job.customer вполне допустимо.
3. Политика UNLINK не поддерживается для ссылок на
коллекции с отношениями one-to-many и many-to-many: при
попытке удаления экземпляра сущности на ведущей
стороне ассоциации будет выброшено исключение
UnsupportedOperationException. Пример ошибочной
политики:
Owner.java
5.2.1.4.3. 5.2.1.4.3. Ограничение уникальности на уровне БД
В режиме мягкого удаления для ограничения
@JoinTable(name = "SAMPLE_OWNER_SUBORDINATE_LINK",
joinColumns = @JoinColumn(name = "OWNER_ID"),
inverseJoinColumns = @JoinColumn(name =
"SUBORDINATE_ID"))
@OnDelete(DeletePolicy.UNLINK)
@ManyToMany
protected List<Subordinate> subordinate;
В режиме мягкого удаления для ограничения
уникальности некоторого значения необходимо
обеспечить существование единственной неудаленной
записи с этим значением, и произвольного количества
удаленных записей с этим же значением.
Реализуется данная логика путем, специфичным для
используемого сервера базы данных:
Если сервер БД поддерживает частичные (partial)
индексы (например, PostgreSQLPostgreSQL), то ограничение
уникальности можно создать следующим образом:
Если сервер БД не поддерживает частичные индексы
(например, Microsoft SQL Server 2005Microsoft SQL Server 2005), то в уникальный
индекс можно включить поле DELETE_TSDELETE_TS:
5.2.2. Metadata Framework5.2.2. Metadata Framework
Для эффективной работы с моделью данных в CUBA-
приложениях используется фреймворк метаданных,
который:
предоставляет удобный интерфейс для получения
информации о сущностях, их атрибутах и отношениях
между сущностями; а также для навигации по ссылкам
служит специализированной и более удобной в
использовании альтернативой Java Reection APIJava Reection API
регламентирует допустимые типы данных и отношений
между сущностями
create unique index IDX_SEC_USER_UNIQ_LOGIN on
SEC_USER (LOGIN_LC) where DELETE_TS is null
create unique index IDX_SEC_USER_UNIQ_LOGIN on
SEC_USER (LOGIN_LC, DELETE_TS)
между сущностями
позволяет создавать универсальные механизмы работы
с данными
5.2.2.1. 5.2.2.1. Интерфейсы метаданных
Рассмотрим основные интерфейсы метаданных.
Рисунок 9. 9. Интерфейсы фреймворка метаданных
Session
Точка входа в фреймворк метаданных. Позволяет
получать экземпляры MetaClass по имени и по
соответствующему классу Java. Обратите внимание на
различие методов getClass() и getClassNN() - первые
могут возвращать null, вторые нет (NonNull).
Объект Session может быть получен через интерфейс
Объект Session может быть получен через интерфейс
инфраструктуры Metadata.
Пример:
MetaModel
Редко используемый интерфейс, служит для
группировки мета-классов.
Группировка осуществляется по имени корневого Java
пакета проекта, указываемого в файле metadata.xml.
MetaClass
Интерфейс метаданных класса сущности. MetaClass
всегда ассоциирован с классом Java, которого он
представляет.
Основные методы:
getName() – имя сущности, по соглашению первой
частью имени до знака $ является код пространства
имен, например, sales$Customer
getProperties() – список мета-свойств (MetaProperty)
getProperty(), getPropertyNN() - получение мета-
свойства по имени. Первый метод в случае отсутствия
атрибута с указанным именем возвращает null,
второй выбрасывает исключение.
Пример:
@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 =
session.getClassNN("sec$User");
MetaClass metaClass2 =
session.getClassNN(User.class);
assert metaClass1 == metaClass2;
Пример:
getPropertyPath() - позволяет перемещаться по
ссылкам. Данный метод принимает строковый
параметр - путь из имен атрибутов, разделенных
точкой. Возвращаемый объект MetaPropertyPath
позволяет обратиться к искомому (последнему в пути)
атрибуту вызовом getMetaProperty().
Пример:
getJavaClass() – класс сущности, которому
соответствует данный MetaClass
getAnnotations() – коллекция мета-аннотаций
MetaProperty
Интерфейс метаданных атрибута сущности.
Основные методы:
getName() – имя свойства, соответствует имени
атрибута сущности
getDomain() – мета-класс, которому принадлежит
MetaClass userClass =
session.getClassNN(User.class);
MetaProperty groupProperty =
userClass.getPropertyNN("group");
MetaClass userClass =
session.getClassNN(User.class);
MetaProperty groupNameProp =
userClass.getPropertyPath("group.name").getMetaProp
erty();
assert
groupNameProp.getDomain().getName().equals("sec$Gro
up");
getDomain() – мета-класс, которому принадлежит
данное свойство
getType() – тип свойства:
простой тип: DATATYPE
перечисление: ENUM
ссылочный тип двух видов:
ASSOCIATION − простая ссылка на другую сущность.
Например, отношение заказа и покупателя −
ассоциация.
COMPOSITION − ссылка на сущность, которая не имеет
самостоятельного значения без владеющей
сущности. COMPOSITION можно считать "более тесным"
отношением, чем ASSOCIATION. Например, отношение
заказа и пункта этого заказа − COMPOSITION, т.к. пункт
не может существовать без заказа, которому он
принадлежит.
Вид ссылочного атрибута ASSOCIATION или
COMPOSITION влияет на режим редактирования
сущности: в первом случае сохранение связанной
сущности в базу данных происходит независимо, а во
втором − связанная сущность сохраняется в БД
только вместе с владеющей сущностью.
getRange() – интерфейс Range, детально
описывающий тип данного атрибута
isMandatory() – признак обязательности атрибута.
Используется, например, визуальными компонентами
для сигнализации пользователю о необходимости
ввода значения.
isReadOnly() – признак неизменности атрибута
getInverse() – для ссылочного атрибута возвращает
мета-свойство с обратной стороны ассоциации, если
таковое имеется
getAnnotatedElement() – поле
getAnnotatedElement() – поле
(java.lang.reflect.Field) или метод
(java.lang.reflect.Method), соответствующие
данному атрибуту сущности
getJavaType() – класс Java данного атрибута
сущности. Это либо тип поля класса, либо тип
возвращаемого значения метода.
getDeclaringClass() – класс Java, содержащий
данный атрибут
Range
Интерфейс, детально описывающий тип атрибута
сущности.
Основные методы:
isDatatype() – возвращает true для атрибута
простого типа
asDatatype() - возвращает Datatype для атрибута
простого типа
isEnum() – возвращает true для атрибута типа
перечисления
asEnumeration() - возвращает Enumeration для
атрибута типа перечисления
isClass() – возвращает true для ссылочного атрибута
типа ASSOCIATION или COMPOSITION
asClass() - возвращает мета-класс ассоциированной
сущности для ссылочного атрибута
isOrdered() – возвращает true если атрибут
представляет собой упорядоченную коллекцию
(например, List)
getCardinality() – вид отношения для ссылочного
атрибута: ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY,
MANY_TO_MANY
5.2.2.2. 5.2.2.2. Формирование метаданных
Основной источник формирования структуры метаданных
- аннотированные классы сущностей.
Класс сущности отражается в метаданных в следующих
случаях:
Класс персистентной сущности аннотирован @Entity,
@Embeddable, @MappedSuperclass и расположен в
пределах корневого пакета, указанного в metadata.xml.
Класс неперсистентной сущности аннотирован
@MetaClass и расположен в пределах корневого пакета,
указанного в metadata.xml.
Все сущности внутри одного корневого пакета
помещаются в один экземпляр MetaModel, которому
присваивается имя этого пакета. Между сущностями
внутри одной MetaModel можно устанавливать
произвольные связи, между разными - в порядке
объявления файлов metadata.xml в свойстве
cuba.metadataConfig.
Атрибут сущности отражается в метаданных, если:
поле класса аннотировано @Column, @OneToOne,
@OneToMany, @ManyToOne, @ManyToMany, @Embedded
поле класса или метод доступа на чтение (getter)
аннотирован @MetaProperty
Параметры мета-класса и мета-свойств формируются на
основе параметров перечисленных аннотаций, а также
типов полей и методов класса. Кроме того, если у
атрибута отсутствует метод доступа на запись (setter),
атрибут становится неизменяемым (read only).
5.2.2.3. Datatype5.2.2.3. Datatype
Интерфейс Datatype определяет методы конвертации
значение в строку и из строки (formatting & parsing). Каждый
атрибут сущности, не являющийся ссылкой, имеет
некоторый Datatype, который и используется
фреймворком для конвертации значений данного
атрибута.
Экземпляры Datatype регистрируются в бине
DatatypeRegistry, который выполняет загрузку и
инициализацию классов реализации Datatype из файлов
metadata.xml компонентов приложения и самого проекта.
Datatype атрибута сущности может быть получен из
соответствующего meta-property методом
getRange().asDatatype().
Кроме конвертации значений атрибутов сущностей,
зарегистрированные экземпляры Datatype могут быть
использованы для преобразования в строку и из строки
произвольных значений поддерживаемых типов. Для
этого необходимо получить экземпляр Datatype из
DatatypeRegistry с помощью его методов get(Class) или
getNN(Class), передавая тип Java, который необходимо
конвертировать.
Datatype сопоставляется атрибуту сущности по
следующим правилам:
Как правило, атрибуту сопоставляется экземпляр
Datatype, зарегистрированный в DatatypeRegistry и
предназначенный для конвертации типа атрибута.
Например, в данном случае атрибут amount получит
BigDecimalDatatype:
@Column(name = "AMOUNT")
private BigDecimal amount;
потому что в cuba-metadata.xml есть следующий
элемент:
Для поля или метода можно задать аннотацию
@MetaProperty, указав в ней атрибут datatype.
Например, атрибут issueYear получит тип
YearDatatype:
если файл metadata.xml проекта содержит следующий
элемент:
Как видно, атрибут datatype аннотации @MetaProperty
указывает на идентификатор, который использован при
регистрации класса имплементации Datatype в файле
metadata.xml.
Основные методы интерфейса Datatype:
format() - преобразовывает переданное значение в
<datatype id="decimal"
class="com.haulmont.chile.core.datatypes.impl.BigDec
imalDatatype"
default="true"
format="0.####" decimalSeparator="."
groupingSeparator=""/>
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
<datatype id="year"
class="com.company.sample.YearDatatype"/>
format() - преобразовывает переданное значение в
строку
parse() - преобразовывает строку в значение нужного
типа
getJavaClass() – возвращает тип Java, для конвертации
которого создан данный Datatype. Этот метод имеет
реализацию по умолчанию, которая считывает значение
аннотации @JavaClass, если она присутствует на
классе.
Datatype определяет два набора методов для
форматирования/парсинга: с учетом локали и без учета
локали. Преобразование с учетом локали используется
повсеместно в пользовательском интерфейсе,
преобразование без учета локали используется в
системных механизмах, например, для сериализации в
REST API.
Форматы для преобразований без учета локали задаются
в коде имплементации или в файле metadata.xml.
В следующем разделе описано, как задать форматы
преобразований с учетом локали.
5.2.2.3.1. 5.2.2.3.1. Строки форматов Datatype Datatype
Форматы для преобразований с учетом локали задаются в
главном пакете локализованных сообщений проекта или
его компонентов, в строках со следующими ключами:
numberDecimalSeparator - задает символ разделителя
целой и дробной части для числовых типов
numberGroupingSeparator - задает символ разделителя
групп разрядов для числовых типов
integerFormat - формат для типов Integer и Long
doubleFormat - формат для типа Double
decimalFormat - формат для типа BigDecimal
dateTimeFormat - формат для типа java.util.Date
dateTimeFormat - формат для типа java.util.Date
dateFormat - формат для типа java.sql.Date
timeFormat - формат для типа java.sql.Time
trueString - строка, соответствующая Boolean.TRUE
falseString - строка, соответствующая Boolean.FALSE
Форматы для используемых в приложении языков
могут быть заданы в Studio. Для этого откройте на
редактирование Project PropertiesProject Properties, нажмите кнопку в
поле Available localesAvailable locales, затем нажмите Show data formatShow data format
stringsstrings.
Строки форматов могут быть получены из бина
FormatStringsRegistry.
5.2.2.3.2. 5.2.2.3.2. Пример специализированного Datatype Datatype
В качестве примера рассмотрим следующую задачу: в
приложении есть атрибуты сущностей, хранящие годы в
виде целых чисел. Пользователи должны иметь
возможность просматривать и редактировать годы,
причем если пользователь вводит только две цифры,
приложение должно преобразовать их в год между 2000 и
2100. В противном случае, все введенное число считается
годом.
Создайте класс в модуле globalglobal:
package com.company.sample.entity;
import com.google.common.base.Strings;
import com.haulmont.chile.core.annotations.JavaClass;
import com.haulmont.chile.core.datatypes.Datatype;
import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
@JavaClass(Integer.class)
public class YearDatatype implements Datatype<Integer>
{
private static final String PATTERN = "##00";
@Override
public String format(@Nullable Object value) {
if (value == null)
return "";
DecimalFormat format = new
DecimalFormat(PATTERN);
return format.format(value);
}
@Override
public String format(@Nullable Object value,
Locale locale) {
return format(value);
}
@Nullable
@Override
public Integer parse(@Nullable String value)
throws ParseException {
if (Strings.isNullOrEmpty(value))
return null;
DecimalFormat format = new
DecimalFormat(PATTERN);
int year = format.parse(value).intValue();
if (year > 2100 || year < 0)
throw new ParseException("Invalid year",
Затем добавьте элемент datatypes в файл metadata.xml
вашего проекта:
В элементе datatype можно также указать атрибут
sqlType, содержащий SQL-тип вашей базы данных,
подходящий для хранения значений нового типа. Этот
SQL-тип будет использоваться CUBA Studio при генерации
скриптов базы данных. Studio может автоматически
определить SQL-тип для следующих типов Java:
java.lang.Boolean
java.lang.Integer
throw new ParseException("Invalid year",
0);
if (year < 100)
year += 2000;
return year;
}
@Nullable
@Override
public Integer parse(@Nullable String value,
Locale locale) throws ParseException {
return parse(value);
}
}
<metadata
xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<datatypes>
<datatype id="year"
class="com.company.sample.entity.YearDatatype"/>
</datatypes>
<!-- ... -->
</metadata>
java.lang.Integer
java.lang.Long
java.math.BigDecimal
java.lang.Double
java.lang.String
java.util.Date
java.util.UUID
byte[]
В нашем случае класс предназначен для работы с типом
Integer (что декларируется аннотацией @JavaClass со
значением Integer.class), поэтому атрибут sqlType
можно не указывать.
Наконец, укажите новый тип данных для требуемых
атрибутов (программно или с помощью интерфейса
Studio):
После выполнения перечисленных действий атрибут
latitude везде в приложении будет отображаться в
нужном формате.
5.2.2.3.3. 5.2.2.3.3. Пример форматирования даты в UI UI
Рассмотрим отображение атрибута Order.date в таблице
браузера заказов.
order-browse.xml
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
<table id="ordersTable">
<columns>
<column id="date"/>
Атрибут date в классе Order определен с типом "дата":
Если текущий пользователь зарегистрирован c русской
локалью, то из главного пакета локализованных
сообщений извлекается строка:
В результате дата "2012-08-06" конвертируется в строку
"06.08.2012" для отображения в ячейке таблицы.
5.2.2.3.4. 5.2.2.3.4. Примеры форматирования дат и чисел в коде
приложения
Если вам необходимо отформатировать или получить из
строки значения BigDecimal, Integer, Long, Double,
Boolean или Date учитывая локаль текущего
пользователя, используйте бин DatatypeFormatter.
Например:
<!--...-->
@Column(name = "DATE", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;
dateFormat=dd.MM.yyyy
@Inject
private DatatypeFormatter formatter;
void sample() {
String dateStr =
formatter.formatDate(dateField.getValue());
// ...
}
Ниже приведены примеры использования методов
интерфейса Datatype напрямую.
Пример форматирования даты
Пример форматирования числового значения с 5
знаками после запятой в Web Client:
com/sample/sales/web/messages_ru.propertiescom/sample/sales/web/messages_ru.properties
@Inject
protected UserSessionSource userSessionSource;
@Inject
protected DatatypeRegistry datatypes;
void sample() {
Date date;
// ...
String dateStr =
datatypes.getNN(Date.class).format(date,
userSessionSource.getLocale());
// ...
}
coordinateFormat = #,##0.00000
@Inject
protected Messages messages;
@Inject
protected UserSessionSource userSessionSource;
@Inject
protected FormatStringsRegistry
formatStringsRegistry;
5.2.2.4. 5.2.2.4. Мета--аннотации
Мета-аннотации сущностей - набор пар ключ/значение,
содержащих дополнительную информацию о сущностях.
Обращение к мета-аннотациям производится с помощью
метода мета-класса getAnnotations().
Источниками мета-аннотаций сущности являются:
Аннотации @OnDelete, @OnDeleteInverse, @Extends. При
этом в мета-аннотациях создаются служебные объекты
связей между сущностями.
Расширяемые мета-аннотации, помеченные аннотацией
@MetaAnnotation. Эти аннотации конвертируются в
мета-аннотации с ключом, соответствующими полному
имени класса Java аннотации и значением, являющимся
map атрибутов аннотации. Например, аннотация
@TrackEditScreenHistory будет иметь значение,
являющееся map с единственным элементом: value →
true. Платформа предоставляет следующие аннотации
такого вида: @NamePattern, @SystemLevel,
@EnableRestore, @TrackEditScreenHistory. В вашем
void sample() {
String coordinateFormat =
messages.getMainMessage("coordinateFormat");
FormatStrings formatStrings =
formatStringsRegistry.getFormatStrings(userSessionSo
urce.getLocale());
NumberFormat format = new
DecimalFormat(coordinateFormat,
formatStrings.getFormatSymbols());
String formattedValue = format.format(value);
// ...
}
@EnableRestore, @TrackEditScreenHistory. В вашем
приложении или компоненте можно создать
собственные аннотации и пометить их аннотацией
@MetaAnnotation.
Опционально: в файлах metadata.xml также могут быть
определены мета-аннотации сущностей. Если мета-
аннотация в XML имеет то же имя, что и мета-аннотация,
созданная по Java аннотации класса сущности, первая
переопределит значение второй.
Пример переопределения мета-аннотаций в
metadata.xml:
<metadata
xmlns="http://schemas.haulmont.com/cuba/metadata.xsd
">
<!-- ... -->
<annotations>
<entity
class="com.company.customers.entity.Customer">
<annotation
name="com.haulmont.cuba.core.entity.annotation.Track
EditScreenHistory">
<attribute name="value" value="true"
datatype="boolean"/>
</annotation>
<property name="name">
<annotation name="length"
value="200"/>
</property>
<property name="customerGroup">
<annotation
name="com.haulmont.cuba.core.entity.annotation.Looku
p">
5.2.3. 5.2.3. Представления
При извлечении сущностей из базы данных обычно встает
вопрос - как обеспечить загрузку связанных сущностей на
нужную глубину?
Например, для браузера Заказов нужно отобразить дату и
сумму заказа совместно с названием Покупателя, т.е.
загрузить связанный экземпляр Покупателя. А для экрана
редактирования Заказа необходимо загрузить еще и
коллекцию Пунктов заказа, причем каждый Пункт заказа
должен содержать связанный экземпляр Товара для
p">
<attribute name="type"
class="com.haulmont.cuba.core.entity.annotation.Look
upType" value="DROPDOWN"/>
<attribute name="actions"
datatype="string">
<value>lookup</value>
<value>open</value>
</attribute>
</annotation>
</property>
</entity>
<entity
class="com.company.customers.entity.CustomerGroup">
<annotation
name="com.haulmont.cuba.core.entity.annotation.Enabl
eRestore">
<attribute name="value"
value="false" datatype="boolean"/>
</annotation>
</entity>
</annotations>
</metadata>
должен содержать связанный экземпляр Товара для
отображения его наименования.
Загрузка по требованию в большинстве случаев не может
помочь, так как обработка данных, как правило,
происходит не в транзакции, в которой загружаются
сущности, а, например, на клиентском уровне в
пользовательском интерфейсе. В то же время задание
жадной загрузки в аннотациях сущностей недопустимо,
так как приводит к постоянному извлечению всего графа
связанных сущностей, который может быть очень
большим.
Другой похожей проблемой является ограничение набора
локальных атрибутов сущностей загружаемого графа:
например, некоторая сущность имеет 50 атрибутов, в том
числе BLOB, а в экране отображается только 10 атрибутов.
Зачем загружать из БД, затем сериализовать и
передавать клиенту 40 атрибутов, которые ему в данный
момент не нужны?
Механизм
представлений
(views) решает эти проблемы,
обеспечивая извлечение из базы данных и передачу
клиенту графов сущностей, ограниченных в глубину и по
атрибутам.
Представление
является описателем графа
объектов, который требуется в некотором экране UI или
другом процессе обработки данных.
Обработка представлений производится следующим
образом:
Все связи в модели данных объявляются с признаком
загрузки по требованию (fetch = FetchType.LAZY, см.
Аннотации сущностей).
В процессе загрузки данных через DataManager
клиентский код помимо JPQL-запроса указывает нужное
представление.
На основе представления формируется так называемая
На основе представления формируется так называемая
FetchGroup
- особенность лежащего в основе слоя ORM
фреймворка EclipseLinkEclipseLink. Fetch Group влияет на
формирование SQL-запроса к базе данных: как на список
возвращаемых полей, так и на соединения с другими
таблицами, содержащими связанные сущности.
Рисунок 10. 10. Классы представления
Представление определяется экземпляром класса View, в
котором:
entityClass - класс сущности, для которого определено
представление. Другими словами, "корень" дерева
загружаемых сущностей.
name - имя представления. Должно быть либо null, либо
уникальным в пределах данной сущности.
properties - коллекция экземпляров класса ViewProperty,
соответствующих загружаемым атрибутам сущности.
includeSystemProperties - признак включения
системных атрибутов (входящих в состав базовых
интерфейсов персистентных сущностей BaseEntity и
Updatable).
Класс ViewProperty имеет следующие свойства:
name - имя атрибута сущности
view - для ссылочных атрибутов задает представление,
с которым необходимо загружать связанную сущность
fetch - для ссылочных атрибутов задает способ
загрузки связанной сущности из БД. Соответствует
перечислению FetchMode:
перечислению FetchMode:
AUTO - платформа автоматически выбирает
оптимальный режим в зависимости от типа отношения.
UNDEFINED - загрузка будет выполнена по правилам JPA,
что означает загрузку отдельными SELECT-запросами.
JOIN - загрузка в том же SELECT-запросе путем
объединения с таблицей, содержащей ссвязанную
сущность.
BATCH - загрузка экземпляров связанной сущности
будет осуществляться порциями. Подробнее см. здесь.
Если атрибут fetch не указан, будет использоваться
режим AUTO. Если атрибут представляет собой
кэшируемую сущность, независимо от указанного
значения будет использоваться UNDEFINED.
Независимо от набора атрибутов, определенного в
представлении, всегда загружаются следующие
атрибуты:
id - идентификатор сущности
version - для оптимистично блокируемых
сущностей, реализующих Versioned
deleteTs, deletedBy - для сущностей, реализующих
SoftDelete
При попытке прочитать или установить значение
незагруженного (не включенного в представление)
атрибута генерируется исключение. Проверить,
загружен ли некоторый атрибут можно методом
PersistenceHelper.isLoaded().
Незагруженные атрибуты имеют значение null. По
умолчанию попытка установки значения незагруженного
атрибута (вызов setter) для Detached сущности вызывает
исключение.
Следует иметь в виду, что незагруженные ссылочные
атрибуты Detached сущности, соответствующие внешним
ключам (т.е. many-to-one, one-to-one), можно установить в
новое ненулевое значение в любом случае, и изменения
будут сохранены при последующем merge().
5.2.3.1. 5.2.3.1. Создание представлений
Представление может быть создано двумя путями:
программно - созданием экземпляра View, например:
Как правило, таким способом создаются представления,
используемые только в каком-то одном месте бизнес-
логики.
декларативно - путем создания описателя на XML и его
развертывания в репозитории представлений
ViewRepository. При развертывании на основе XML-
описателя создаются и кэшируются экземпляры View. В
дальнейшем в любом месте кода приложения
требуемое представление можно получить вызовом
репозитория с указанием класса сущности и имени
представления.
Рассмотрим подробнее декларативный способ создания и
работы с представлениями.
View view = new View(Order.class)
.addProperty("date")
.addProperty("amount")
.addProperty("customer", new
View(Customer.class)
.addProperty("name")
);
работы с представлениями.
ViewRepository является бином SpringSpring, доступным для
всех блоков приложения. Ссылка на ViewRepository
может быть также получена через интерфейс
инфраструктуры Metadata. Для получения экземпляра
View, содержащегося в репозитории, используются
методы getView(). Для развертывания XML-описателей
представлений в репозитории используются методы
deployViews() базовой реализации
AbstractViewRepository.
В репозитории для каждой сущности по умолчанию
доступны три представления с именами _local, _minimal
и _base:
_local включает в себя все локальные атрибуты
сущности
_minimal включает в себя атрибуты, входящие в имя
экземпляра сущности, и задаваемые аннотацией
@NamePattern. Если аннотация @NamePattern для
сущности не указана, данное представление не
включает никаких атрибутов.
_base включает в себя все локальные несистемные
атрибуты и атрибуты, заданные в аннотации
@NamePattern (т.е. фактически _minimal + _local).
Подробная структура XML-описателей изложена здесь.
Пример описателя представления для сущности Заказ,
которое должно обеспечить загрузку всех локальных
атрибутов, ассоциированного Покупателя и коллекции
Пунктов заказа:
<view class="com.sample.sales.entity.Order"
name="orderWithCustomer"
extends="_local">
<property name="customer" view="_minimal"/>
Рекомендуемый способ группировки и развертывания
описателей представлений:
В модуле globalglobal в корне src создать файл views.xml и
поместить в него все описатели представлений,
которые должны быть доступны глобально, т.е. на всех
уровнях приложения.
Зарегистрировать данный файл в свойстве
cuba.viewsCong блока Middleware и используемых
клиентских блоков, т.е. в файле app.properties модуля
corecore, в файле web-app.properties модуля webweb и так
далее. Это обеспечит автоматическое развертывание
представлений на старте приложения в репозитории
Middleware и клиентских блоков (см. метод
AbstractViewRepository.init()).
Если существуют представления, которые необходимы
только какому-то одному клиентскому блоку
приложения, то можно определить их в аналогичном
файле данного блока, например, web-views.xml, и
добавить этот файл в свойство cuba.viewsConfig этого
блока, т.е. в данном случае в файл web-app.properties.
Если на момент развертывания некоторого
представления в репозитории уже есть представление
для этого же класса сущности и с таким же именем, то
новое будет проигнорировано. Для того чтобы
представление заменило имеющееся в репозитории и
гарантированно было развернуто, в XML-описателе
должен быть явно указан атрибут overwrite = "true".
Рекомендуется давать представлениям
"описательные" имена. Например, не "browse", а
<property name="customer" view="_minimal"/>
<property name="items" view="itemInOrder"/>
</view>
"описательные" имена. Например, не "browse", а
"customerBrowse". Это упрощает поиск XML-описателей
представлений по имени в процессе разработки
приложения.
5.2.4. 5.2.4. Управляемые бины
Управляемые бины (Managed Beans)
− это программные
компоненты, предназначенные для реализации бизнес-
логики приложения. Термин "управляемые" в данном
случае означает, что созданием экземпляров и
установкой связей между такими компонентами
управляет контейнер который является основной частью
фреймворка SpringSpring.
Managed Bean представляет собой
singleton
, то есть в
некотором блоке приложения существует только
один экземпляр данного класса. Поэтому, если бин
содержит изменяемые данные в полях (другими
словами, имеет состояние), то обращение к таким
данным необходимо синхронизировать.
5.2.4.1. 5.2.4.1. Создание бина
Для создания управляемого бина достаточно добавить
классу Java аннотацию
@org.springframework.stereotype.Component. Например:
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;
@Component(OrderWorker.NAME)
public class OrderWorker {
Рекомендуется присваивать бину уникальное имя вида
{имя_проекта}_{имя_класса}, и определять его в
константе NAME.
Аннотация @javax.annotation.ManagedBean также
может ипользоваться для определения бина, однако
ее наличие может вызывать проблемы при
развертывании в некоторые сервера приложений.
Поэтому мы рекомендуем использовать только
аннотацию @Component из Spring Framework.
Класс управляемого бина должен находиться внутри
дерева пакетов с корнем, заданным в элементе
context:component-scan файла spring.xml. В нашем случае
файл spring.xml содержит элемент:
что означает, что поиск аннотированных бинов для
данного блока приложения будет происходить начиная с
пакета com.sample.sales.
Управляемые бины можно создавать на любом уровне, так
как контейнер Spring Framework используется во всех
стандартных блоках приложения.
public class OrderWorker {
public static final String NAME =
"sales_OrderWorker";
public void calculateTotals(Order order) {
}
}
<context:component-scan base-
package="com.sample.sales"/>
5.2.4.2. 5.2.4.2. Использование бина
Ссылку на бин можно получить с помощью инжекции или
класса AppBeans. В качестве примера использования бина
рассмотрим реализацию сервиса OrderService,
делегирующего выполнение бину OrderWorker:
package com.sample.sales.core;
import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import
org.springframework.transaction.annotation.Transaction
al;
import javax.inject.Inject;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService
{
@Inject
protected Persistence persistence;
@Inject
protected OrderWorker orderWorker;
@Transactional
@Override
public BigDecimal calculateTotals(Order order) {
Order entity =
persistence.getEntityManager().merge(order);
orderWorker.calculateTotals(entity);
}
}
В данном примере сервис стартует транзакцию, вносит
полученный с клиентского уровня экземпляр сущности в
персистентный контекст, и передает управление бину
OrderWorker, который и содержит основную бизнес-
логику.
5.2.5. JMX-5.2.5. JMX-бины
Иногда требуется предоставить администратору системы
возможность просматривать и изменять состояние
некоторого управляемого бина во время выполнения. В
этом случае рекомендуется создать JMX-бин -
программный компонент, имеющий JMX-интерфейс. Такой
бин, как правило, делегирует вызовы управляемому бину,
содержащему кэш, конфигурационные данные или
статистику, к которым нужно обеспечить доступ через
JMX.
Как видно из диаграммы, JMX-бин состоит из интерфейса и
класса реализации. Класс должен представлять собой
управляемый бин, то есть иметь аннотацию @Component и
уникальное имя. Интерфейс JMX-бина специальным
уникальное имя. Интерфейс JMX-бина специальным
образом регистрируется в spring.xml для создания в
текущей JVM собственно JMX-интерфейса.
Вызовы всех методов интерфейса JMX-бина
перехватываются с помощью Spring AOPSpring AOP
классом−интерцептором MBeanInterceptor, который
обеспечивает установку правильного ClassLoader в
контексте потока выполнения, и журналирование
необработанных исключений.
Интерфейс JMX-бина обязательно должен иметь имя
вида {имя_класса}MBean.
С JMX-интерфейсом можно работать из внешних
инструментов, таких как jconsole или jvisualvm. Кроме
того, в состав блока Web Client платформы входит JMX-
консоль, предоставляющая базовые средства просмотра
состояния и вызова методов JMX-бинов.
5.2.5.1. 5.2.5.1. Создание JMX- JMX-бина
Рассмотрим процесс создания JMX-бина на примере.
Интерфейс JMX-бина:
package com.sample.sales.core;
import org.springframework.jmx.export.annotation.*;
@ManagedResource(description = "Performs operations
on Orders")
public interface OrdersMBean {
@ManagedOperation(description = "Recalculates an
order amount")
Интерфейс и его методы могут содержать аннотации
для задания описания JMX-бина и его операций. Это
описание будет отображаться во всех инструментах,
работающих с данным JMX-интерфейсом, тем самым
помогая администратору системы.
Аннотацию @JmxRunAsync можно использовать для
указания длительных операций. Если такая операция
запускается через встроенную консоль JMX, платформа
отображает диалог с неопределенным индикатором
прогресса и кнопкой CancelCancel. Пользователь может
прервать операцию и продолжить работу с
приложением. Аннотация может также содержать
параметр timeout, который устанавливает
максимальное время выполнения в миллисекундах,
например:
Если таймаут превышен, диалог закрывается с
сообщением об ошибке.
Пожалуйста имейте в виду, что если операция
прервана пользователем или по таймауту, она все
равно продолжает работать в фоне, то есть данные
действия не прерывают самого выполнения, а
только возвращают управление пользователю.
@ManagedOperationParameters({@ManagedOperationParame
ter(name = "orderId", description = "")})
String calculateTotals(String orderId);
}
@JmxRunAsync(timeout = 30000)
String calculateTotals();
Так как инструменты JMX поддерживают ограниченный
набор типов данных, параметры и результат метода
желательно задавать типа String, и при
необходимости выполнять конвертацию внутри
метода. Помимо String, поддерживаются следующие
типы параметров: boolean, double, float, int, long,
Boolean, Integer.
Класс JMX-бина:
package com.sample.sales.core;
import com.haulmont.cuba.core.*;
import com.haulmont.cuba.core.app.*;
import com.sample.sales.entity.Order;
import
org.apache.commons.lang.exception.ExceptionUtils;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.UUID;
@Component("sales_OrdersMBean")
public class Orders implements OrdersMBean {
@Inject
protected OrderWorker orderWorker;
@Inject
protected Persistence persistence;
@Authenticated
@Override
public String calculateTotals(String orderId) {
try {
try (Transaction tx =
persistence.createTransaction()) {
Order entity =
Аннотация @Component определяет, что данный класс
является управляемым бином с именем
sales_OrdersMBean. Имя указано напрямую в аннотации,
а не в константе, так как доступ к JMX-бину из кода Java
не требуется.
Рассмотрим реализацию метода calculateTotals().
Метод имеет аннотацию @Authenticated, т.е. при входе
в метод и при отсутствии в потоке выполнения
пользовательской сессии выполняется системная
аутентификация.
Тело метода обернуто в блок try/catch, так что метод в
случае успешного выполнения возвращает строку
"Done", а в случае ошибки - stacktrace исключения в виде
строки.
В данном случае все исключения обрабатываются, а
значит, не попадают в MBeanInterceptor и не
выводятся в журнал автоматически. Поэтому при
необходимости логировать исключения здесь нужно
добавить вызов логгера в секции catch.
Логика метода заключается в том, что он стартует
транзакцию, загружает экземпляр сущности Order по
Order entity =
persistence.getEntityManager().find(Order.class,
UUID.fromString(orderId));
orderWorker.calculateTotals(entity);
tx.commit();
};
return "Done";
} catch (Throwable e) {
return ExceptionUtils.getStackTrace(e);
}
}
}
идентификатору и передает управление бину
OrderWorker для обработки.
Регистрация JMX-бина в spring.xml:
Все JMX-бины проекта объявляются в одном экземпляре
MBeanExporter в элементах map/entry свойства beans.
Ключом элемента здесь является JMX ObjectName,
значением - имя бина, заданное в аннотации @Component.
ObjectName начинается с имени веб-приложения, так как
в одном экземпляре сервера приложения (т.е. в одной
JVM) может быть развернуто несколько веб-
приложений, экспортирующих одинаковые JMX-
интерфейсы.
5.2.5.2. JMX-5.2.5.2. JMX-бины платформы
В данном разделе описаны некоторые имеющиеся в
платформе JMX-бины.
5.2.5.2.1. CachingFacadeMBean5.2.5.2.1. CachingFacadeMBean
CachingFacadeMBean предоставляет методы очистки
<bean id="sales_MBeanExporter" lazy-init="false"
class="com.haulmont.cuba.core.sys.jmx.MBeanExporter"
>
<property name="beans">
<map>
<entry
key="${cuba.webContextName}.sales:type=Orders"
value-ref="sales_OrdersMBean"/>
</map>
</property>
</bean>
CachingFacadeMBean предоставляет методы очистки
различных кэшей в блоках Middleware и Web Client.
JMX ObjectName: app-core.cuba:type=CachingFacade и
app.cuba:type=CachingFacade
5.2.5.2.2. CongStorageMBean5.2.5.2.2. CongStorageMBean
ConfigStorageMBean позволяет просматривать и задавать
значения свойствам приложения в блоках Middleware, Web
Client и Web Portal.
Данный интерфейс имеет отдельные наборы операций
для работы с параметрами конфигурации и
развертывания (*AppProperties) и с параметрами
времени выполнения (*DbProperties). Эти операции
отображают только свойства, явно заданные в
хранилище. То есть если имеется конфигурационный
интерфейс, определяющий некоторое свойство и его
значение по умолчанию, но в базе данных или в файлах
никакого значения не указано, данные операции не
отобразят свойство и его текущее значение.
Заметьте, что изменения в свойствах, хранящихся в
файлах, не персистентны и актуальны только до
перезапуска блока приложения.
В отличие от операций, описанных выше, операция
getConfigValue() всегда возвращает в точности то
значение, какое вернул бы соответствующий метод
конфигурационного интерфейса, вызванный из кода
приложения.
JMX ObjectName:
app-core.cuba:type=ConfigStorage
app.cuba:type=ConfigStorage
app-portal.cuba:type=ConfigStorage
5.2.5.2.3. EmailerMBean5.2.5.2.3. EmailerMBean
EmailerMBean позволяет просмотреть текущие значения
параметров отсылки email, а также отправить тестовое
сообщение.
JMX ObjectName: app-core.cuba:type=Emailer
5.2.5.2.4. PersistenceManagerMBean5.2.5.2.4. PersistenceManagerMBean
PersistenceManagerMBean предоставляет следующие
возможности:
управление механизмом статистики сущностей
отображение новых скриптов обновления БД методом
findUpdateDatabaseScripts() и запуск обновления
методом updateDatabase()
запуск произвольных JPQL запросов в контексте
Middleware методами jpqlLoadList(),
jpqlExecuteUpdate()
JMX ObjectName: app-core.cuba:type=PersistenceManager
5.2.5.2.5. ScriptingManagerMBean5.2.5.2.5. ScriptingManagerMBean
ScriptingManagerMBean является JMX-фасадом для
интерфейса инфраструктуры Scripting.
JMX ObjectName: app-core.cuba:type=ScriptingManager
JMX-атрибуты:
RootPath - абсолютный путь к конфигурационному
каталогу блока приложения, в котором запущен данный
бин.
JMX-операции:
runGroovyScript() - выполнить скрипт Groovy в
контексте Middleware и вернуть результат. В скрипт
передаются следующие переменные:
persistence типа Persistence
metadata типа Metadata
configuration типа Conguration
dataManager типа DataManager
Для отображения в JMX-интерфейсе результат должен
быть типа String. В остальном аналогичен методу
Scripting.runGroovyScript().
Пример скрипта, создающего набор тестовых
пользователей:
import com.haulmont.cuba.core.*
import com.haulmont.cuba.core.global.*
import com.haulmont.cuba.security.entity.*
PasswordEncryption passwordEncryption =
AppBeans.get(PasswordEncryption.class)
Transaction tx = persistence.createTransaction()
try {
EntityManager em =
persistence.getEntityManager()
Group group = em.getReference(Group.class,
UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-
dff348347f93'))
for (i in (1..250)) {
User user = new User()
user.setGroup(group)
user.setLogin("user_${i.toString().padLeft(3,
'0')}")
user.setName(user.login)
user.setPassword(passwordEncryption.getPasswordHash(
user.id, '1'));
em.persist(user)
}
tx.commit()
5.2.5.2.6. ServerInfoMBean5.2.5.2.6. ServerInfoMBean
ServerInfoMBean предоставляет общую информацию о
данном блоке Middleware: номер и дату сборки,
идентификатор сервера.
JMX ObjectName: app-core.cuba:type=ServerInfo
5.2.6. 5.2.6. Интерфейсы инфраструктуры
Интерфейсы инфраструктуры обеспечивают доступ к
часто используемой функциональности платформы.
Большинство из этих интерфейсов расположены в модуле
globalglobal и могут быть использованы как на среднем слое, так
и в блоках клиентского уровня, но некоторые (например,
Persistence) доступны только коду среднего слоя.
Интерфейсы инфраструктуры реализуются бинами Spring
Framework, поэтому они могут быть инжектированы в
любые другие управляемые компоненты (Managed Beans,
сервисы среднего слоя, контроллеры экранов
универсального пользовательского интерфейса).
Кроме того, как и любые другие бины, интерфейсы
инфраструктуры могут быть получены с помощью
статических методов класса AppBeans и использоваться в
неуправляемых компонентах (POJO, вспомогательных
классах и пр.).
5.2.6.1. Conguration5.2.6.1. Conguration
Позволяет получать ссылки на конфигурационные
интерфейсы там, где невозможна их инжекция.
tx.commit()
} finally {
tx.end()
}
интерфейсы там, где невозможна их инжекция.
Пример:
5.2.6.2. Messages5.2.6.2. Messages
Интерфейс Messages обеспечивает получение
локализованных строк сообщений.
// field injection
@Inject
protected Configuration configuration;
...
String tempDir =
configuration.getConfig(GlobalConfig.class).getTempDir
();
// setter injection
protected GlobalConfig globalConfig;
@Inject
public void setConfiguration(Configuration
configuration) {
this.globalConfig =
configuration.getConfig(GlobalConfig.class);
}
// location
String tempDir =
AppBeans.get(Configuration.class).getConfig(GlobalConf
ig.class).getTempDir();
локализованных строк сообщений.
Рассмотрим методы интерфейса подробнее.
getMessage() - возвращает локализованное сообщение
по ключу, имени пакета сообщений и требуемой локали.
Существует несколько модификаций данного метода в
зависимости от набора параметров. Если локаль не
указана в параметре метода, используется локаль
текущего пользователя.
Примеры:
formatMessage() - находит локализованное сообщение
по ключу, имени пакета сообщений и требуемой локали,
и использует его для форматирования переданных
параметров. Формат задается по правилам метода
String.format(). Существует несколько модификаций
данного метода в зависимости от набора параметров.
Если локаль не указана в параметре метода,
используется локаль текущего пользователя.
Пример:
@Inject
protected Messages messages;
...
String message1 = messages.getMessage(getClass(),
"someMessage");
String message2 =
messages.getMessage("com.abc.sales.web.customer",
"someMessage");
String message3 =
messages.getMessage(RoleType.STANDARD);
String formattedValue =
messages.formatMessage(getClass(), "someFormat",
someValue);
getMainMessage() - возвращает локализованное
сообщение из главного пакета данного блока
приложения.
Пример:
getMainMessagePack() - возвращает имя главного
пакета сообщений данного блока приложения.
Пример:
getTools() - возвращает экземпляр интерфейса
MessageTools (см. ниже).
5.2.6.2.1. MessageTools5.2.6.2.1. MessageTools
ManagedBean, содержащий вспомогательные методы
работы с локализованными сообщениями. Интерфейс
MessageTools можно получить либо методом
Messages.getTools(), либо как любой другой бин -
инжекцией или через класс AppBeans.
Методы MessageTools:
loadString() - возвращает локализованное сообщение,
заданное ссылкой вида msg://{messagePack}/{key}.
someValue);
protected Messages messages =
AppBeans.get(Messages.class);
...
messages.getMainMessage("actions.Ok");
String formattedValue =
messages.formatMessage(messages.getMainMessagePack()
, "someFormat", someValue);
Составные части ссылки:
msg:// - обязательный префикс.
{messagePack} - необязательное имя пакета сообщения.
Если не указано, предполагается, что имя пакета
передается в loadString() отдельным параметром.
{key} - ключ сообщения в пакете.
Примеры ссылок на сообщения:
getEntityCaption() - возвращает локализованное
название сущности.
getPropertyCaption() - возвращает локализованное
название атрибута сущности.
hasPropertyCaption() - определяет, задано ли для
атрибута сущности локализованное название.
getLocValue() - возвращает локализованное значение
атрибута сущности, основываясь на определении
аннотации @LocalizedValue.
getMessageRef() - формирует для мета-свойства ссылку
на сообщение, по которой можно получить
локализованное название атрибута сущности.
getDefaultLocale() - возвращает локаль приложения
по умолчанию, то есть указанную первой в списке
свойства cuba.availableLocales.
useLocaleLanguageOnly() - возвращает true, если в
списке поддерживаемых приложением локалей,
заданном свойством cuba.availableLocales, для всех
локалей определен только язык, а country и variant не
указаны. Этим методом пользуются механизмы
платформы, которым необходимо найти наиболее
msg://someMessage
msg://com.abc.sales.web.customer/someMessage
подходящую локаль из списка поддерживаемых на
основе локали, полученной из внешних источников,
таких как операционная система или HTTP запрос.
trimLocale() - удаляет из переданной локали все кроме
языка, если метод useLocaleLanguageOnly()
возвращает true.
Для расширения набора вспомогательных методов в
конкретном приложении бин MessageTools можно
переопределить. Примеры работы с расширенным
интерфейсом:
5.2.6.3. Metadata5.2.6.3. Metadata
Интерфейс Metadata обеспечивает доступ к сессии
метаданных и репозиторию представлений.
Методы интерфейса:
getSession() - возвращает экземпляр сессии
метаданных
getViewRepository() - возвращает экземпляр
репозитория представлений
getExtendedEntities() - возвращает экземпляр
ExtendedEntities, предназначенный для работы с
расширенными сущностями. Подробнее см. Расширение
сущности
create() - создать экземпляр сущности, учитывая
MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
возможность расширения. Подробнее см. Расширение
сущности
getTools() - возвращает экземпляр интерфейса
MetadataTools (см. ниже).
5.2.6.3.1. MetadataTools5.2.6.3.1. MetadataTools
ManagedBean, содержащий вспомогательные методы
работы с метаданными. Интерфейс MetadataTools можно
получить либо методом Metadata.getTools(), либо как
любой другой бин - инжекцией или через класс AppBeans.
Методы MetadataTools:
getAllPersistentMetaClasses() - возвращает
коллекцию мета-классов персистентных сущностей
getAllEmbeddableMetaClasses() - возвращает
коллекцию мета-классов встраиваемых сущностей
getAllEnums() - возвращает коллекцию классов
перечислений, используемых в качестве типов
атрибутов сущностей
format() - форматирует переданное значение в
соответствии с типом данных заданного мета-свойства
isSystem() - определяет, является ли переданное мета-
свойство системным, т.е. заданным в одном из базовых
интерфейсов сущностей
isPersistent() - определяет, является ли переданное
мета-свойство персистентным, т.е. хранимым в БД
isTransient() - определяет, является ли переданное
мета-свойство или произвольный атрибут
неперсистентным
isEmbedded() - определяет, является ли переданное
мета-свойство встроенным объектом
isAnnotationPresent() - определяет наличие
указанной аннотации на классе или его предках
getNamePatternProperties() - возвращает коллекцию
мета-свойств атрибутов, входящих в имя экземпляра,
возвращаемого методом Instance.getInstanceName().
См. @NamePattern.
Для расширения набора вспомогательных методов в
конкретном приложении бин MetadataTools можно
переопределить. Примеры работы с расширенным
интерфейсом:
5.2.6.4. Resources5.2.6.4. Resources
Обеспечивает загрузку ресурсов по следующим правилам:
1. если указанное местонахождение представляет собой
URL, ресурс загружается из этого URL;
2. если указанное местонахождение начинается с
префикса classpath:, ресурс загружается из classpath;
3. если не URL и не начинается с classpath:, то:
a. в каталоге конфигурации приложения ищется файл,
используя указанное местонахождение как
относительный путь. Если файл найден, ресурс
загружается из него;
b. если ресурс не найден на предыдущих этапах, он
загружается из classpath.
На практике явное указание URL или префикса classpath:
используется редко, т.е. обычно ресурсы загружаются
либо из конфигурационного каталога, либо из classpath.
MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
либо из конфигурационного каталога, либо из classpath.
Ресурс в конфигурационном каталоге замещает
одноименный ресурс в classpath.
Методы Resources:
getResourceAsStream() - возвращает InputStream для
указанного ресурса, либо null, если ресурс не найден.
Поток должен быть закрыт после использования,
например:
Возможно использование "try with resources":
getResourceAsString() - возвращает указанный ресурс
в виде строки, либо null, если ресурс не найден
5.2.6.5. Scripting5.2.6.5. Scripting
@Inject
protected Resources resources;
...
InputStream stream = null;
try {
stream =
resources.getResourceAsStream(resourceLocation);
...
} finally {
IOUtils.closeQuietly(stream);
}
try (InputStream stream =
resources.getResourceAsStream(resourceLocation)) {
...
}
Интерфейс Scripting позволяет динамически (т.е. во
время работы приложения) компилировать и загружать
классы Java и Groovy, а также выполнять скрипты и
выражения на Groovy.
Методы Scripting:
evaluateGroovy() - выполняет выражение на Groovy и
возвращает его результат.
Свойство приложения cuba.groovyEvaluatorImport
позволяет определить общий набор импортируемых
классов, подставляемых в каждое выполняемое
выражение. По умолчанию все стандартные блоки
приложения импортируют класс PersistenceHelper.
Скомпилированные выражения кэшируются, что
значительно ускоряет повторное выполнение.
Пример:
runGroovyScript() - выполняет скрипт Groovy и
возвращает его результат.
Скрипт должен быть расположен либо в
конфигурационном каталоге приложения, либо в
classpath (текущая реализация Scripting поддерживает
@Inject
protected Scripting scripting;
...
Integer intResult = scripting.evaluateGroovy("2 +
2", new Binding());
Binding binding = new Binding();
binding.setVariable("instance", new User());
Boolean boolResult =
scripting.evaluateGroovy("return
PersistenceHelper.isNew(instance)", binding);
classpath (текущая реализация Scripting поддерживает
ресурсы classpath только внутри JAR-файлов). Скрипт в
конфигурационном каталоге замещает одноименный
скрипт в classpath.
Путь к скрипту указывается с разделителями /, в начале
пути символ / не требуется.
Пример:
loadClass() - загружает Java или Groovy класс, используя
следующую последовательность действий:
1. Если класс уже загружен, возвращает его.
2. Ищет исходный текст Groovy (файл *.groovy) в каталоге
конфигурации. Если найден, компилирует его,
загружает и возвращает класс.
3. Ищет исходный текст Java (файл *.java) в каталоге
конфигурации. Если найден, компилирует его,
загружает и возвращает класс.
4. Ищет скомпилированный класс в classpath, если найден -
загружает и возвращает его.
5. Если ничего не найдено, возвращает null.
Файлы исходных текстов Java и Groovy в каталоге
конфигурации можно изменять во время работы
приложения. При следующем вызове loadClass()
соответствующий класс будет перекомпилирован и
возвращен новый, однако существуют следующие
@Inject
protected Scripting scripting;
...
Binding binding = new Binding();
binding.setVariable("itemId", itemId);
BigDecimal amount =
scripting.runGroovyScript("com/abc/sales/CalculatePr
ice.groovy", binding);
возвращен новый, однако существуют следующие
ограничения:
нельзя изменять тип исходного текста с Groovy на Java
если существовал исходный текст Groovy, и был
однажды скомпилирован, то удаление файла
исходного текста не приведет к загрузке другого
класса из classpath - будет по-прежнему возвращаться
класс, скомпилированный из удаленного исходника.
Пример:
getClassLoader() - возвращает ClassLoader, способный
работать по правилам, описанным выше для метода
loadClass().
Кэш скомпилированных классов можно очистить во время
выполнения с помощью JMX-бина CachingFacadeMBean.
См. также ScriptingManagerMBean.
5.2.6.6. Security5.2.6.6. Security
Обеспечивает авторизацию - проверку прав пользователя
на различные объекты системы. Перед вызовом
соответствующих методов UserSession выполняется поиск
исходного мета-класса сущности, что является важным
при наличии расширений. Кроме методов, дублирующих
методы UserSession, данный интерфейс имеет методы
isEntityAttrReadPermitted() и
@Inject
protected Scripting scripting;
...
Class calculatorClass =
scripting.loadClass("com.abc.sales.PriceCalculator"
);
isEntityAttrReadPermitted() и
isEntityAttrUpdatePermitted(), предназначенные для
определения доступности пути к атрибуту с учетом
доступности атрибутов и сущностей, входящих в этот
путь.
Интерфейс Security рекомендуется использовать в
прикладном коде вместо вызовов методов
UserSession.isXYXPermitted().
Подробнее см. Аутентификация пользователей.
5.2.6.7. TimeSource5.2.6.7. TimeSource
Обеспечивает получение текущего времени. Применение
new Date() и т.п. в прикладном коде не рекомендуется.
Примеры:
5.2.6.8. UserSessionSource5.2.6.8. UserSessionSource
Обеспечивает получение объекта сессии текущего
пользователя. Подробнее см. Аутентификация
пользователей.
5.2.6.9. UuidSource5.2.6.9. UuidSource
@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime =
AppBeans.get(TimeSource.class).currentTimeMillis();
5.2.6.9. UuidSource5.2.6.9. UuidSource
Обеспечивает получение значений UUID, в том числе для
идентификаторов сущностей. Применение
UUID.randomUUID() в прикладном коде не рекомендуется.
Для вызова из статического контекста можно
использовать класс UuidProvider, который имеет также
дополнительный метод fromString(), работающий
быстрее, чем стандартный метод UUID.fromString().
5.2.6.10. DataManager5.2.6.10. DataManager
Интерфейс DataManager является универсальным
средством для загрузки графов сущностей из базы
данных, и для сохранения изменений, произведенных в
detached экземплярах сущностей.
В разделе DataManager vs. EntityManager приведена
информация о различиях между DataManager и
EntityManager.
DataManager на самом деле делегирует выполнение
реализациям DataStore, и поддерживает ссылки между
сущностями из разных хранилищ. Большинство деталей
реализации, описанных ниже, актуальны только когда
производится работа через RdbmsStore с сущностями,
хранящимися в реляционной БД. Для другого типа
хранилища все, кроме сигнатур методов, может
отличаться. Для простоты изложения, далее, когда мы
говорим просто "DataManager", мы будем иметь в ввиду
"DataManager через RdbmsStore".
Методы DataManager:
load(), loadList() - загружает граф сущностей в
соответствии с параметрами переданного объекта
соответствии с параметрами переданного объекта
LoadContext.
В LoadContext обязательно должен быть передан либо
JPQL-запрос, либо идентификатор сущности. Если
передано и то и другое, используется запрос, а
идентификатор игнорируется.
Правила создания запросов аналогичны описанным в
разделе Выполнение JPQL-запросов. Отличием является
то, что в запросе LoadContext могут быть использованы
только именованные параметры, позиционные не
поддерживаются.
Примеры загрузки сущностей в контроллере экрана:
@Inject
private DataManager dataManager;
private Book loadBookById(UUID bookId) {
LoadContext<Book> loadContext =
LoadContext.create(Book.class)
.setId(bookId).setView("book.edit");
return dataManager.load(loadContext);
}
private List<BookPublication>
loadBookPublications(UUID bookId) {
LoadContext<BookPublication> loadContext =
LoadContext.create(BookPublication.class)
.setQuery(LoadContext.createQuery("select p from
library$BookPublication p where p.book.id =
:bookId")
.setParameter("bookId", bookId))
.setView("bookPublication.full");
return dataManager.loadList(loadContext);
}
loadValues() - загружает список пар ключ-значение.
Метод принимает ValueLoadContext, в котором
задается запрос и список ключей. Возвращаемый
список содержит экземпляры KeyValueEntity.
Например:
getCount() - возвращает количество записей для
запроса, переданного в метод. Когда возможно, для
максимальной производительности, стандартная
реализация в классе RdbmsStore выполняет запрос
select count() с условиями исходного запроса.
commit() - сохраняет в базе данных набор сущностей,
переданный в объекте CommitContext. Отдельно
указываются коллекции сущностей, которые нужно
сохранить и которые нужно удалить.
Метод возвращает набор экземпляров сущностей,
возвращенных из метода EntityManager.merge(), то есть по
сути свежие экземпляры, только что обновленные в БД.
Дальнейшая работа должна производиться именно с
этими возвращенными экземплярами, чтобы
предотвратить потерю данных или исключения
ValueLoadContext context = ValueLoadContext.create()
.setQuery(ValueLoadContext.createQuery(
"select o.customer,
sum(o.amount) from demo$Order o " +
"where o.date >= :date group by
o.customer")
.setParameter("date", orderDate))
.addProperty("customer")
.addProperty("sum");
List<KeyValueEntity> list =
dataManager.loadValues(context);
оптимистичной блокировки. Для того, чтобы обеспечить
наличие нужных атрибутов у возвращенных сущностей,
с помощью мэп CommitContext.getViews() можно
указать представление для каждого сохраняемого
экземпляра.
Примеры сохранения коллекций сущностей:
reload() - удобный метод для перезагрузки экземпляра
сущности с требуемым представлением. Делегирует
выполнение методу load().
remove() - удаляет экземпляр сущности из базы данных.
Делегирует выполнение методу commit().
@Inject
private DataManager dataManager;
private void saveBookInstances(List<BookInstance>
toSave, List<BookInstance> toDelete) {
CommitContext commitContext = new
CommitContext(toSave, toDelete);
dataManager.commit(commitContext);
}
private Set<Entity>
saveAndReturnBookInstances(List<BookInstance>
toSave, View view) {
CommitContext commitContext = new
CommitContext();
for (BookInstance bookInstance : toSave) {
commitContext.addInstanceToCommit(bookInstance,
view);
}
return dataManager.commit(commitContext);
}
Делегирует выполнение методу commit().
Транзакции
DataManager всегда стартует новую транзакцию и по
завершении работы выполняет коммит, таким образом
возвращая сущности в detached состоянии.
Частичные сущности
По умолчанию, DataManager загружает
частичные
сущности в соответствии с указанными
представлениями. В следующих случаях будут
загружаться все локальные атрибуты и представление
будет определять только загрузку связей:
Загружаемая сущность кэшируется.
Для сущности заданы in-memory "read" ограничения.
Атрибут loadPartialEntities класса LoadContext
установлен в false.
5.2.6.10.1. 5.2.6.10.1. Права доступа в DataManager DataManager
Методы load(), loadList(), loadValues() и getCount()
проверяют наличие у пользователя права READ на
загружаемую сущность. Кроме того, при извлечении
сущностей из БД накладываются ограничения групп
доступа.
Метод commit() проверяет наличие у пользователя права
UPDATE на изменяемые сущности и DELETE на удаляемые.
По умолчанию, DataManager проверяет права на операции
(READ/CREATE/UPDATE/DELETE) с сущностями, когда
вызывается с клиентской стороны, и игнорирует их, когда
вызывается из кода middleware. Права на атрибуты по
умолчанию не проверяются.
Если вы хотите, чтобы DataManager проверял права на
операции и при вызове на среднем слое, получите
методом DataManager.secure() специальный объект-
обертку и вызывайте методы у него. В качестве
обертку и вызывайте методы у него. В качестве
альтернативы, вы можете установить свойство
приложения cuba.dataManagerChecksSecurityOnMiddleware,
чтобы проверка прав работала для всего приложения.
Права на атрибуты будут проверяться только если вы
дополнительно установите свойство приложения
cuba.entityAttributePermissionChecking в true.
Имейте в ввиду, что ограничения групп доступа (row-level
security) применяются всегда, независимо от того, был ли
вызов с клиентского или со среднего слоя.
5.2.6.10.2. 5.2.6.10.2. Запросы с distinct distinct
В JPQL запросах для экранов со списками сущностей, в
которых включено постраничное отображение и
возможна непредсказуемая модификация запроса
универсальным фильтром или механизмом ограничений
групп доступа, при отсутствии в запросе оператора
distinct может возникать следующий эффект:
при объединении с коллекцией на уровне извлечения из
базы данных возникает набор с дубликатами строк
на клиентском уровне в источнике данных дубликаты
исчезают, т.к. попадают в мэп (java.util.Map)
при постраничном отображении на одной странице
оказывается меньшее количество строк, чем запрошено,
общее количество строк наоборот завышено.
Таким образом, рекомендуется в JPQL запросы браузеров
включать предложение distinct, которое гарантирует
отсутствие дубликатов записей при выборке из базы
данных. Однако в некоторых серверах БД (в частности
PostgreSQLPostgreSQL) при большом количестве извлекаемых записей
(более 10000) SQL запрос с distinct выполняется
недопустимо долго.
Для решения этой проблемы в платформе реализована
Для решения этой проблемы в платформе реализована
возможность корректной работы без distinct на уровне
SQL. Данный механизм включается свойством приложения
cuba.inMemoryDistinct, при активации которого выполняется
следующее:
В JPQL запросе должен по-прежнему присутствовать
select distinct
В DataManager из JPQL запроса перед отправкой в ORM
distinct вырезается
После загрузки страницы данных на Middleware
удаляются дубликаты и выполняются дополнительные
запросы к БД для получения нужного количества строк,
которые затем и возвращаются клиенту.
5.2.6.10.3. 5.2.6.10.3. Последовательная выборка
DataManager может выполнять последовательную
выборку данных из результатов предыдущего запроса.
Эта возможность используется в универсальном фильтре
при последовательном наложении фильтров.
Данный механизм работает следующим образом:
При получении LoadContext с установленными
атрибутами prevQueries и queryKey DataManager
выполняет выборку по предыдущему запросу и
сохраняет идентификаторы полученных сущностей в
таблице SYS_QUERY_RESULT (соответствующей сущности
sys$QueryResult), разделяя наборы записей по
идентификаторам пользовательских сессий и ключу
сеанса выборки queryKey.
Текущий запрос модифицируется для объединения с
результатами предыдущего, так что в итоге возвращает
данные, соответствующие условиям обоих запросов,
объединенных по "И".
Далее процесс может повторяться, при этом
уменьшающийся набор предыдущих результатов
уменьшающийся набор предыдущих результатов
удаляется из таблицы SYS_QUERY_RESULT и заполняется
заново.
Таблицу SYS_QUERY_RESULT необходимо периодически
чистить от ненужных результатов запросов, оставленных
завершенными пользовательскими сессиями. Для этого
предназначен метод deleteForInactiveSessions бина
QueryResultsManagerAPI. В прикладном проекте с
включенным параметром cuba.allowQueryFromSelected
необходимо вызывать этот метод из назначенных
заданий, например:
5.2.6.11. Events5.2.6.11. Events
Бин Events реализует функциональность публикации
объектов-событий уровня приложения. События могут
использоваться для передачи данных между слабо
связанными компонентами приложения. Бин Events
является простым фасадом для объекта
ApplicationEventPublisher Spring Framework.
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="cuba_QueryResultsManager"
method="deleteForInactiveSessions" fixed-
rate="600000"/>
</task:scheduled-tasks>
public interface Events {
String NAME = "cuba_Events";
void publish(ApplicationEvent event);
}
Этот бин имеет только один метод - publish(),
принимающий объект события. Метод Events.publish()
уведомляет все слушатели, зарегистрированные в
приложении и подписанные на события того же типа, что
и переданный объект. Вы может использовать класс-
обёртку PayloadApplicationEvent для публикации любых
объектов в качестве событий.
См. также руководство Spring Framework.
Обработка событий в компонентах приложения
Прежде всего, необходимо создать класс события. Он
должен быть наследником класса ApplicationEvent.
Класс события может включать любые дополнительные
данные. Например:
Бины могут публиковать события, используя бин Events:
public class DemoEvent extends ApplicationEvent {
public DemoEvent(User source) {
super(source);
}
@Override
public User getSource() {
return (User) super.getSource();
}
}
@Component
public class DemoBean {
@Inject
private Events events;
@Inject
private UserSessionSource userSessionSource;
По умолчанию все события обрабатываются синхронно.
Есть два способа обработки событий:
Реализовать интерфейс ApplicationListener.
Использовать аннотацию @EventListener для метода.
В первом случае, мы должны создать бин, реализующий
интерфейс ApplicationListener с указанием типа
события:
Второй способ может использоваться для сокрытия
деталей реализации обработчика событий и обработки
множества различных событий в одном бине:
public void demo() {
UserSession userSession =
userSessionSource.getUserSession();
events.publish(new
DemoEvent(userSession.getUser()));
}
}
@Component
public class DemoEventListener implements
ApplicationListener<DemoEvent> {
@Inject
private Logger log;
@Override
public void onApplicationEvent(DemoEvent event)
{
log.debug("Demo event is published");
}
}
Вы можете использовать интерфейс Ordered и
аннотацию @Order Spring Framework для указания порядка
исполнения обработчиков событий. Все бины и
обработчики событий платформы используют значение
order от 100 до 1000, таким образом, вы можете добавить
обработчик событий как до, так и после обработчиков
события платформы. Если вы хотите добавить свой
обработчик события до обработчиков из платформы, то
используйте значение меньше 100.
См. также События логина.
Обработка событий в экранах
Обычно, бин Events делегирует публикацию события
объекту ApplicationContext. Для блоков Web Client /
Desktop Client это поведение отличается от стандартного,
вы можете использовать дополнительный интерфейс
для классов событий - UiEvent. Это интерфейс-маркер
для событий, которые должны быть доставлены в
экраны пользовательского интерфейса текущего
@Component
public class MultipleEventListener {
@Order(10)
@EventListener
private void handleDemoEvent(DemoEvent event) {
// handle event
}
@Order(1010)
@EventListener
private void
handleUserLoginEvent(UserLoggedInEvent event) {
// handle event
}
}
экраны пользовательского интерфейса текущего
экземпляра UI (текущей вкладки веб-браузера). Важно
отметить, что экземпляры событий, реализующих
UiEvent, не доставляются в бины Spring и не могут быть
обработаны за пределами UI.
Пример класса события:
События публикуются при помощи бина Events из
контроллера экрана так же, как и из бинов Spring:
Чтобы обработать событие, вы должны объявить в
экране метод с аннотацией @EventListener (Интерфейс
ApplicationListener не поддерживается):
public class UserRemovedEvent extends
ApplicationEvent implements UiEvent {
public UserRemovedEvent(User source) {
super(source);
}
@Override
public User getSource() {
return (User) super.getSource();
}
}
@Inject
Events events;
// ...
UserRemovedEvent event = new
UserRemovedEvent(removedUser);
events.publish(event);
@Order(15)
Вы можете использовать аннотацию @Order, чтобы
задать порядок вызова обработчиков события.
Если класс события реализует UiEvent, и объект такого
события опубликован при помощи бина Events из потока
UI, то будут вызваны обработчики событий этого типа в
открытых на данный момент окнах и фреймах.
Обработка событий синхронная. Только экраны и
фреймы текущей активной вкладки веб-браузера
получат уведомление о событии.
5.2.7. PersistenceHelper5.2.7. PersistenceHelper
Вспомогательный класс для получения информации о
персистентных сущностях. В отличие от бинов Persistence
и PersistenceTools доступен на всех уровнях приложения.
Методы PersistenceHelper:
isLoaded() - определяет, загружен ли данный атрибут
сущности. Атрибут загружается, если он включен в
представление, или если это локальный атрибут и
никакое представление не использовалось в процессе
загрузки через EntityManager или DataManager. Данный
метод поддерживает только непосредственные
атрибуты сущностей.
isNew() - определяет, является ли переданный
экземпляр только что созданным, т.е. находящимся в
состоянии New. Возвращает true, также если экземпляр
не является персистентной сущностью.
@Order(15)
@EventListener
protected void onUserRemove(UserRemovedEvent event)
{
showNotification("User is removed " +
event.getSource());
}
не является персистентной сущностью.
isManaged() - определяет, находится ли переданный
экземпляр в состоянии Managed, то есть присоединен к
персистентному контексту.
isDetached() - определяет, находится ли переданный
экземпляр в состоянии Detached. Возвращает true, также
если экземпляр не является персистентной сущностью.
isSoftDeleted() - определяет, поддерживает ли
переданный класс сущности мягкое удаление.
getEntityName() - возвращает имя сущности, заданное в
аннотации @Entity.
5.2.8. AppContext5.2.8. AppContext
AppContext - системный класс, в статических полях
которого хранятся ссылки на некоторые общие для
любого блока приложения компоненты:
ApplicationContext фреймворка SpringSpring
Набор свойств приложения, загруженных из файлов
app.properties
ThreadLocal переменная, хранящая экземпляры
SecurityContext
Коллекция слушателей жизненного цикла приложения
(AppContext.Listener)
AppContext инициализируется на запуске приложения
классами-загрузчиками, специфичными для типа блока
приложения:
загрузчик Middleware - AppContextLoader
загрузчик Web Client - WebAppContextLoader
загрузчик Web Portal - PortalAppContextLoader
загрузчик Desktop Client - DesktopAppContextLoader
AppContext может быть использован в прикладном коде
для решения следующих задач:
для решения следующих задач:
Регистрации слушателей, срабатывающих после полной
инициализации и перед закрытием приложения,
например:
В момент вызова applicationStarted():
Полностью инициализированы все бины, в том числе
выполнены их методы @PostConstruct.
Можно использовать статические методы получения
бинов AppBeans.get().
Метод AppContext.isStarted() возвращает true.
Метод AppContext.isReady() возвращает false.
В блоке Middleware: если свойство приложения
cuba.automaticDatabaseUpdate включено, все скрипты
обновления БД успешно выполнены.
В момент вызова applicationStopped():
Все бины работоспособны и доступны через
статические методы AppBeans.get().
Метод AppContext.isStarted() возвращает false.
Метод AppContext.isReady() возвращает false.
AppContext.addListener(new AppContext.Listener() {
@Override
public void applicationStarted() {
System.out.println("Application is ready");
}
@Override
public void applicationStopped() {
System.out.println("Application is
closing");
}
});
Метод AppContext.isReady() возвращает false.
Практический пример использования
AppContext.Listener см. в Выполнение кода на старте
приложения.
Получения значений свойств приложения, хранимых в
файлах app.properties, если они недоступны через
конфигурационные интерфейсы.
Передачи SecurityContext в новые потоки выполнения,
см. Аутентификация пользователей.
5.2.9. 5.2.9. Свойства приложения
Свойства приложения − именованные значения
различных типов, определяющие всевозможные аспекты
конфигурации и функционирования приложения.
Свойства приложения широко используются в платформе,
и могут применяться в приложении для решения
аналогичных задач.
По назначению свойства приложения можно
классифицировать следующим образом:
Конфигурационные параметры
- задают наборы
конфигурационных файлов и некоторые параметры
пользовательского интерфейса, т.е. определяют
функциональность приложения. Значения
конфигурацинных параметров обычно задаются при
разработке приложения.
Например: cuba.springContextCong.
Параметры развертывания
- различные URL для
соединения блоков приложения, тип используемой БД,
настройки безопасности и т.д. Значения параметров
развертывания обычно зависят от окружения, в котором
устанавливается данный экземпляр приложения.
Например: cuba.connectionUrlList, cuba.dbmsType,
cuba.userSessionExpirationTimeoutSec.
cuba.userSessionExpirationTimeoutSec.
Параметры времени выполнения
- активность аудита,
параметры отсылки email и т.д. Параметры времени
выполнения могут быть изменены при необходимости
во время работы приложения без его перезапуска.
Например: cuba.entityLog.enabled, cuba.email.smtpHost.
Задание свойств приложения
Значения свойств приложения могут быть заданы в базе
данных, в файлах свойств, или через системные свойства
Java. Кроме того, значение, заданное в файле,
переопределяет одноименное значение, заданное в БД.
Значение, заданное системным свойством Java,
переопределяет одноименные значения из файлов и из
БД.
Некоторые свойства не поддерживают установку
свойств в базе данных по причине того, что их значения
требуются еще то того, как БД становится доступной
приложению. Это параметры конфигурации и
развертывания. Поэтому их можно устанавливать
только в файлах свойств или через системные свойства
Java. Параметры времени выполнения всегда могут быть
установлены в базе данных (и, возможно,
переопределены в файле или системными свойствами).
Как правило, некоторое свойство используется только в
одном или нескольких блоках приложения. Например,
cuba.persistenceCong необходимо только для Middleware,
cuba.web.appWindowMode − только для Web Client, а
cuba.springContextCong − для всех блоков. Это означает,
что если нужно задать значение некоторому свойству,
это необходимо сделать
во всех блоках
, в которых
данное свойство используется. Свойства, хранящиеся в
БД, доступны всем блокам, поэтому они
устанавливаются в одном месте (в таблице базы
данных), независимо от того, в каких блоках они
данных), независимо от того, в каких блоках они
используются. Более того, платформа предоставляет
экран Administration > Application PropertiesAdministration > Application Properties для управления
свойствами, хранящимися в БД. Свойства, хранящиеся в
файлах, должны быть установлены одновременно в
соответствующих файлах блоков приложения.
Когда вам необходимо установить значение
свойству приложения, определенному платформой,
найдите это свойство в документации. Если в
документации сказано, что свойство хранится в БД,
для установки значения используйте экран
Administration > Application PropertiesAdministration > Application Properties. В противном
случае выясните в документации, какие блоки
приложения используют свойство, и установите
значение в файлах app.properties этих блоков.
Например, если в документации сказано, что
свойство используется во всех блоках, а ваше
приложение состоит из Middleware и Web Client,
установите свойство в файле app.properties
модуля corecore и в файле web-app.properties модуля
webweb. Параметры развертывания можно также
установить вне проекта в конфигурационном
каталоге. Подробнее см. Хранение свойств в
файлах.
Свойства из компонентов приложения
Компонент приложения может предоставлять свойства
путем объявления их в файле app-component.xml. Тогда
если приложение, использующее компонент, не задает
собственное значение свойства, значение будет
получено из компонента. Если приложение использует
несколько компонентов, предоставляющих одно и то же
свойство, значение будет получено из компонента,
который является ближайшим предком в иерархии
зависимостей между компонентами. Если существует
зависимостей между компонентами. Если существует
несколько компонентов на одном уровне иерархии, то
значение свойства непредсказуемо.
Аддитивные свойства
Иногда необходимо получить объединенное значение
свойства из всех компонентов, используемых в
приложении. Это особенно актуально для
конфигурационных параметров, которые позволяют
механизмам платформы конфигурировать приложение
на основании свойств, предоставляемых компонентами
приложения.
Такие свойства должны быть сделаны
аддитивными
путем добавления знака плюс в начале значения. Этот
знак говорит о том, что значение свойства во время
выполнения должно быть собрано из компонентов
приложения. Например, cuba.persistenceCong -
аддитивное свойство. В вашем проекте оно задает файл
persistence.xml, определяющий модель данных
проекта. Однако вследствие того, что реальное
значение свойства будет также включать файлы
persistence.xml компонентов приложения, полная
модель данных вашего приложения будет включать
также и сущности, определенные в компонентах.
Если знак + опустить, то значение будет получено только
из текущего проекта. Это может быть полезным в
случае, если наследование некоторой конфигурации из
компонентов не требуется, например, при определении
структуры главного меню.
Программный доступ к свойствам приложения
Доступ к свойствам из кода приложения можно
получить следующими способами:
Через конфигурационный интерфейс. Если
определить свойства с помощью аннотированных
определить свойства с помощью аннотированных
методов конфигурационного интерфейса, то код
приложения будет иметь типизированный доступ к
этим свойствам. Конфигурационные интерфейсы
позволяют работать со свойствами всех типов
хранения: в базе данных, в файлах и системных
свойствах.
Методом getProperty() класса AppContext. Если вы
установили свойство в файле или в системном
свойстве Java, то код приложения может прочитать
значение с помощью этого метода. Данный подход
имеет следующие недостатки:
Не поддерживаются свойства, хранящиеся в базе
данных.
В отличие от вызова метода интерфейса, вам
необходимо передавать имя свойства в строке.
В отличие от получения результата нужного типа, вы
можете получить только строковое значение
свойства.
5.2.9.1. 5.2.9.1. Хранение свойств в файлах
Свойства, определяющие конфигурацию и параметры
развертывания, задаются в специальных файлах свойств,
имеющих имя вида *app.properties. Каждый блок
приложения имеет набор таких файлов, который
определяется следующим образом:
Для блоков, являющихся веб-приложениями (Middleware,
Web Client, Web Portal) набор файлов свойств задается в
web.xml в параметре appPropertiesConfig.
Для блока Desktop Client основной способ задания набора
файлов свойств − переопределение в приложении
метода getDefaultAppPropertiesConfig() в классе-
наследнике com.haulmont.cuba.desktop.App.
наследнике com.haulmont.cuba.desktop.App.
Например, набор файлов свойств блока Middleware
задается в файле web/WEB-INF/web.xml модуля corecore, и
выглядит следующим образом:
Здесь префикс classpath: означает, что данный файл
нужно искать в Java classpath, префикс file: − в файловой
системе. Путь без такого префикса означает путь внутри
веб-приложения относительно его корня. Возможно
использование системных свойств Java, в данном случае
это catalina.home − путь к каталогу установки Tomcat.
Порядок перечисления файлов важен, так как значения,
указанные в каждом последующем файле, заменяют
значения одноименных свойств, заданные в предыдущих
файлах.
Последний файл в приведенном наборе −
local.app.properties. Он может использоваться для
переопределения свойств приложения при
развертывании. Если этого файла нет, он игнорируется.
Если же во время инсталляции системы требуется
переопределение некоторых параметров (как правило,
различных URL), достаточно создать этот файл и
поместить в него переопределяемые свойства. При
последующих обновлениях системы такой файл с
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
"file:${catalina.base}/conf/app-
core/local.app.properties"
</param-value>
</context-param>
последующих обновлениях системы такой файл с
локальными настройками легко сохранить. В разделе
Использование Tomcat при эксплуатации приложения
приведен пример использования файла
local.app.properties.
Аналогом local.app.properties для Desktop Client служат
аргументы командной строки запуска JVM. Загрузчик
свойств данного блока воспринимает все аргументы,
содержащие знак "=", как пары ключ-значение, и заменяет
ими соответствующие свойства приложения, заданные в
файлах app.properties.
Правила задания информации в файлах *.properties:
Кодировка файла - UTF-8
Ключ может состоять из латинских букв, цифр, точек и
знаков подчеркивания
Значение пишется после знака равно (=)
Значение не нужно брать в кавычки " или '
Файловые пути записываются либо в UNIX-виде
(/opt/haulmont/), либо в Windows-виде
(c:\\haulmont\\)
Возможно использование кодов \n \t \r. Символ \
является зарезервированным, для вставки в значение
экранируется сам собой (\\). Подробнее см.:
http://docs.oracle.com/javase/tutorial/java/data/characters.html
Для ввода значения в нескольких строках файла
используйте символ \ в конце строки, для того чтобы
данное значение продолжалось на следующей
строке.
5.2.9.2. 5.2.9.2. Хранение свойств в базе
данных
данных
Свойства приложения, представляющие собой параметры
времени выполнения, хранятся в таблице SYS_CONFIG базы
данных.
Такие свойства имеют следующие особенности:
Так как значение свойства хранится в базе данных, оно
задается в одном месте, независимо от того, в каких
блоках приложения оно используется.
Значение может быть изменено и сохранено во время
работы приложения следующими способами:
Через экран Administration > Application PropertiesAdministration > Application Properties.
Через JMX бин CongStorageMBean.
Если конфигурационный интерфейс, содержащий это
свойство, имеет соответствующий setter, то свойство
может изменено кодом приложения.
Значение свойства может быть переопределено для
конкретного блока приложения в его файле
app.properties или одноименным системным
свойством Java.
Следует иметь в виду, что на клиентском уровне чтение
свойства, хранящегося в БД, приводит к запросу к
Middleware, что менее эффективно, чем чтение свойства из
локального файла app.properties. Для уменьшения
количества таких запросов клиент кэширует все свойства,
хранящиеся в БД, на время жизни экземпляра реализации
конфигурационного интерфейса. Поэтому если, например,
в некотором экране UI необходимо несколько раз
обратиться к свойствам одного конфигурационного
интерфейса, лучше получить ссылку на него при
инициализации экрана, и сохранить в поле для
последующих обращений к одному и тому же экземпляру.
5.2.9.3. 5.2.9.3. Конфигурационные
интерфейсы
Данный механизм позволяет работать со свойствами
приложения через методы Java-интерфейсов, что дает
следующие преимущества:
Типизированность - прикладной код работает с
нужными типами (String, Boolean, Integer и пр.), а не только
со строками.
В прикладном коде вместо строковых идентификаторов
свойств используются методы интерфейсов, имена
которых проверяются компилятором и подсказываются
средой разработки.
Пример получения значения таймаута транзакции в блоке
Middleware:
При невозможности инжекции можно получить ссылку на
конфигурационный интерфейс через Conguration:
@Inject
private ServerConfig serverConfig;
public void doSomething() {
int timeout =
serverConfig.getDefaultQueryTimeoutSec();
...
}
int timeout = AppBeans.get(Configuration.class)
.getConfig(ServerConfig.class)
.getDefaultQueryTimeoutSec();
Конфигурационные интерфейсы не являются
нормальными бинами Spring, не пытайтесь получить
их через AppBeans.get() - только непосредственной
инжекцией самого интерфейса или через
Configuration.getConfig().
5.2.9.3.1. 5.2.9.3.1. Использование
Для создания конфигурационного интерфейса
необходимо:
Создать интерфейс, унаследованный от
com.haulmont.cuba.core.config.Config (не путать с
классом сущности
com.haulmont.cuba.core.entity.Config)
Добавить интерфейсу аннотацию @Source для указания
источника (способа хранения) параметров:
SourceType.SYSTEM - значение свойства будет взято из
системных свойств данной JVM, т.е. методом
System.getProperty().
SourceType.APP - значение свойства будет взято из
файлов app.properties.
SourceType.DATABASE - значение свойства будет взято
из базы данных.
Создать методы доступа к свойству (getter / setter). Если
значение свойства не предполагается изменять из кода
приложения, метод доступа на запись не нужен. Тип,
вовращаемый методом доступа на чтение, определяет
тип свойства. Возможные типы рассмотрены ниже.
Добавить методу доступа на чтение аннотацию
@Property, определяющую имя свойства.
Опционально аннотацию @Source можно задать для
отдельного свойства в интерфейсе, если его источник
отличается от заданного для всего интерфейса.
отличается от заданного для всего интерфейса.
Например:
Создавать класс реализации конфигурационного
интерфейса не нужно - при получении ссылки на
интерфейс инжекцией или через Conguration будет
автоматически создан необходимый прокси-объект.
5.2.9.3.2. 5.2.9.3.2. Типы свойств
"Из коробки" платформой поддерживаются следующие
типы свойств:
String, простые типы и их объектные обертки (boolean,
Boolean, int, Integer, etc.).
Перечисления (enum). Значение свойства сохраняется в
файле или БД в виде имени значения перечисления.
Если перечисление реализует интерфейс EnumClass и
имеет статический метод fromId() для получения
значения по идентификатору, с помощью аннотации
@EnumStore можно задать хранение значения в виде
идентификатора. Например:
@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {
@Property("sales.companyName")
String getCompanyName();
}
@Property("myapp.defaultCustomerGrade")
@DefaultInteger(10)
@EnumStore(EnumStoreMode.ID)
CustomerGrade getDefaultCustomerGrade();
@EnumStore(EnumStoreMode.ID)
Классы персистентных сущностей. При обращении к
свойству типа сущности происходит загрузка из БД
экземпляра, заданного значением свойства.
Для поддержки произвольного типа необходимо
реализовать классы TypeStringify и TypeFactory для
преобразования значения в строку и из нее, и указать эти
классы для свойства с помощью аннотаций @Stringify и
@Factory.
Рассмотрим этот процесс на примере типа UUID.
Создаем класс
com.haulmont.cuba.core.config.type.UuidTypeFactory
унаследованный от
com.haulmont.cuba.core.config.type.TypeFactory и
реализуем в нем метод:
TypeStringify создавать не нужно, т.к. по умолчанию
будет использован метод toString() − в данном случае
он нам подходит.
Аннотируем свойство в конфигурационном интерфейсе:
@EnumStore(EnumStoreMode.ID)
void setDefaultCustomerGrade(CustomerGrade grade);
public Object build(String string) {
if (string == null) {
return null;
}
return UUID.fromString(string);
}
@Factory(factory = UuidTypeFactory.class)
UUID getUuidProp();
void setUuidProp(UUID value);
В платформе определены реализации TypeFactory для
следующих типов:
UUID - UuidTypeFactory, описано выше.
java.util.Date - DateFactory. Значение даты должно
быть указано в формате yyyy-MM-dd HH:mm:ss.SSS,
например:
List<Integer> (список целых чисел) -
IntegerListTypeFactory. Значение свойства должно
быть указано в виде списка чисел, разделенных
пробелами, например:
List<String> (список строк) - StringListTypeFactory.
Значение свойства должно быть указано в виде списка
строк, разделенных символом "|", например:
5.2.9.3.3. 5.2.9.3.3. Значения по умолчанию
Для свойств конфигурационных интерфейсов могут быть
заданы значения по умолчанию. Эти значения будут
возвращаться вместо null, если данный параметр не
задан в месте хранения - в БД или в файле
app.properties.
Значение по умолчанию может быть задано в виде строки
с помощью аннотации @Default, либо в виде конкретного
void setUuidProp(UUID value);
cuba.test.dateProp = 2013-12-12 00:00:00.000
cuba.test.integerListProp = 1 2 3
cuba.test.stringListProp = aaa|bbb|ccc
с помощью аннотации @Default, либо в виде конкретного
типа с помощью других аннотаций пакета
com.haulmont.cuba.core.config.defaults:
Для сущностей значение по умолчанию задается строкой
вида {entity_name}-{id}-{optional_view_name},
например:
@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();
@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();
@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();
@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();
@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();
@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();
5.2.10. 5.2.10. Локализация сообщений
Приложение на основе платформы CUBA поддерживает
локализацию сообщений, то есть вывод всех элементов
пользовательского интерфейса на языке, выбранном
пользователем.
Возможности выбора языка пользователем определяются
комбинацией свойств приложения cuba.localeSelectVisible и
cuba.availableLocales.
Для того, чтобы некоторое сообщение могло быть
локализовано, т.е. представлено пользователю на нужном
языке, его необходимо поместить в так называемый
пакет
сообщений
. Ниже рассмотрены принципы работы
механизма локализации и правила создания сообщений.
Получение локализованных сообщений содержит
информацию о способах получения локализованных
сообщений в различных компонентах системы.
5.2.10.1. 5.2.10.1. Пакеты сообщений
Пакет сообщений представляет собой набор файлов
свойств с именами вида messages{_XX}.properties,
расположенных в одном Java-пакете. Суффикс XX
определяет язык, для которого в данном файле
содержатся сообщения, и соответствует коду языка в
@Default("sec$User-98e5e66c-3ac9-11e2-94c1-
3860770d7eaf-browse")
User getAdminUser();
@Default("sec$Role-a294aef0-3ac9-11e2-9433-
3860770d7eaf")
Role getAdminRole();
содержатся сообщения, и соответствует коду языка в
Locale.getLanguage(). Возможно также использование
остальных атрибутов Locale, например, country. В этом
случая файл пакета будет иметь вид
messages{
XX_YY}.properties. Один из файлов пакета
может быть без суффикса языка - это _файл по
умолчанию
. Именем пакета сообщений считается имя
Java-пакета, в котором расположены файлы пакета.
Рассмотрим пример:
Данный пакет состоит из 3-х файлов - один для русского
языка, один для французского, и один по умолчанию. Имя
пакета - com.abc.sales.gui.customer
Файлы сообщений содержат пары ключ-значение, где
ключ - это идентификатор сообщения, на который
ссылается код приложения, а значение - само сообщение
на языке данного файла. Правила задания пар
аналогичны правилам файлов свойств
java.util.Properties, со следующими особенностями:
Кодировка файла - обязательно UTF-8
Поддерживается включение других пакетов сообщений
с помощью ключа @include, в том числе нескольких
сразу - перечислением через запятую. При этом если
некоторый ключ сообщения встречается и во
включаемом пакете, и в текущем, будет использовано
сообщение из текущего. Пример включения пакетов:
/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties
@include=com.haulmont.cuba.web, com.abc.sales.web
someMessage=Some Message
Получение сообщений из пакетов производится с
помощью методов интерфейса Messages по следующим
правилам:
Сначала производится поиск в конфигурационном
каталоге приложения
Ищется файл messages_XX.properties в каталоге,
задаваемом именем пакета сообщений, где XX - код
требуемого языка
Если такого файла нет, в этом же каталоге ищется
файл по умолчанию messages.properties
Если найден или файл нужного языка, или файл по
умолчанию, он загружается вместе со всеми @include, и
в нем ищется ключ сообщения
Если файл не найден, либо нужный ключ в нем
отсутствует, производится смена каталога на
родительский, и процедура поиска повторяется. И так
до достижения корня конфигурационного каталога.
Если в конфигурационном каталоге сообщение не
найдено, производится поиск в classpath по такому же
алгоритму.
Если сообщение найдено, оно кэшируется и
возвращается. Если не найдено - кэшируется факт
отсутствия сообщения и возвращается ключ, который
был передан для поиска. Таким образом, сложная
процедура поиска выполняется только один раз, в
дальнейшем результат загружается из локального для
блока приложения кэша.
Рекомендуется организовывать пакеты сообщений
следующим образом:
someMessage=Some Message
...
следующим образом:
Если приложение не предполагает
интернационализации, то можно не использовать
пакеты и включать строки сообщений прямо в код
приложения, либо пользоваться файлами по
умолчанию messages.properties для отделения
ресурсов от кода.
Если приложение интернациональное, логично
файлы по умолчанию использовать для языка
основной аудитории приложения, либо для
английского языка. Именно сообщения из файлов
по умолчанию будут показаны пользователю, если
сообщений для нужного языка не найдено.
5.2.10.2. 5.2.10.2. Главный пакет сообщений
Каждый стандартный блок приложения определяет для
себя один
главный
пакет сообщений. Для блоков
клиентского уровня этот пакет содержит названия
пунктов главного меню и общих элементов UI (например,
названия кнопок OKOK и CancelCancel). Для всех блоков
приложения, включая Middleware, главный пакет
определяет форматы преобразований Datatype.
Для указания главного пакета сообщений используется
свойство приложения cuba.mainMessagePack. Значением
свойства может быть либо один пакет, либо список
пакетов, разделенный пробелами. Например:
В данном случае сообщения, заданные во втором пакете
списка, будут перекрывать сообщения из первого пакета.
cuba.mainMessagePack=com.haulmont.cuba.web
com.abc.sales.web
списка, будут перекрывать сообщения из первого пакета.
Таким образом, в проекте приложения можно
переопределять сообщения, заданные в пакетах
компонентов приложения.
Сообщения, заданные в пакетах базовых проектов CUBA,
также можно переопределять в главном пакете
сообщений проекта:
5.2.10.3. 5.2.10.3. Локализация названий
сущностей и атрибутов
Для отображения в UI локализованных названий
сущностей и их атрибутов необходимо создать
специальные пакеты сообщений в тех же Java-пакетах,
что и сами сущности. Формат файлов сообщений должен
быть следующим:
Ключ названия сущности - простое имя класса (без
пакета)
Ключ названия атрибута - простое имя класса, затем
через точку имя атрибута
Пример русской локализации сущности
com.abc.sales.entity.Customer - файл
/com/abc/sales/entity/messages_ru.properties:
com.haulmont.cuba.gui.backgroundwork/backgroundWorkPro
gress.timeoutMessage = Новое сообщение об ошибке
Customer=Покупатель
Customer.name=Имя
Customer.email=Email
Order=Заказ
Такие пакеты сообщений, как правило, используются
неявно для разработчика, например, визуальными
компонентами Table и FieldGroup. Кроме того, названия
сущностей и атрибутов могут быть также получены
следующими методами:
программно - методами MessageTools
getEntityCaption(), getPropertyCaption()
в XML-дескрипторе экрана - указанием ссылки на
сообщение по правилам MessageTools.loadString:
msg://{entity_package}/{key}, например,
5.2.10.4. 5.2.10.4. Локализация enum enum
Для локализации названий и значений перечислений
необходимо в пакет сообщений, находящийся в Java-
пакете класса перечисления добавить сообщения со
следующими ключами:
Ключ названия перечисления - простое имя класса (без
пакета)
Ключ значения - простое имя класса, затем через точку
имя значения
Например, для перечисления
Order=Заказ
Order.customer=Покупатель
Order.date=Дата
Order.amount=Сумма
caption="msg://com.abc.sales.entity/Customer.name"
package com.abc.sales;
файл русской локализации
/com/abc/sales/messages_ru.properties должен
содержать строки:
Локализованные значения перечислений автоматически
используются различными визуальными компонентами,
например, LookupField. Для программного получения
локализованного значения перечисления можно
использовать метод getMessage() интерфейса Messages,
просто передавая в него экземпляр enum.
5.2.11. 5.2.11. Аутентификация пользователей
В данном разделе рассмотрены некоторые аспекты
управления доступом с точки зрения разработчика
приложения. Для получения полной информации о
возможностях и настройке ограничения доступа
пользователей к данным см. Подсистема безопасности.
5.2.11.1. UserSession5.2.11.1. UserSession
Основной элемент подсистемы контроля доступа в CUBA-
приложении - пользовательская сессия. Это объект класса
UserSession, который ассоциирован с
аутентифицированным в данный момент в системе
пользователем, и содержит информацию о правах
public enum CustomerGrade { PREMIUM, HIGH, STANDARD }
CustomerGrade=Уровень покупателя
CustomerGrade.PREMIUM=Премиум
CustomerGrade.HIGH=Высокий
CustomerGrade.STANDARD=Стандартный
доступа пользователя к данным. Объект текущей сессии
может быть получен в любом блоке приложения через
интерфейс инфраструктуры UserSessionSource.
Пользовательская сессия создается на Middleware при
выполнении метода AuthenticationManager.login()
после аутентификации пользователя по переданному
имени и паролю. Объект UserSession затем кэшируется в
данном блоке Middleware, и возвращается на клиентский
уровень. При работе в кластере объект сессии
реплицируется на соседние узлы кластера Middleware.
Клиентский блок, получив объект сессии, также сохраняет
его у себя, так или иначе ассоциируя с активным
пользователем (например, в HTTP сессии). Далее все
вызовы Middleware для данного пользователя
сопровождаются передачей идентификатора сессии
(типа UUID), причем прикладному коду не нужно об этом
заботиться - идентификатор сессии передается
автоматически, независимо от сигнатуры вызываемых
методов среднего слоя. Обработка вызовов клиентов на
Middleware начинается с извлечения из кэша сессии по
полученному идентификатору и установки ее в потоке
выполнения. Объект сессии удаляется из кэша при вызове
метода LoginService.logout(), либо при истечении
времени бездействия, определяемого свойством
приложения cuba.userSessionExpirationTimeoutSec.
Таким образом, идентификатор сессии, создаваемой при
входе пользователя в систему, служит для
аутентификации пользователя при каждом вызове
среднего слоя.
Объект UserSession содержит также методы для
авторизации
текущего пользователя, т.е. проверки его
прав на объекты системы: isScreenPermitted(),
isEntityOpPermitted(), isEntityAttrPermitted(),
isSpecificPermitted().
С объектом UserSession могут быть ассоциированы
именованные атрибуты произвольного сериализуемого
типа. Атрибуты устанавливаются методом
setAttribute() и возвращаются методом getAttribute().
Последний может также возвращать следующие
параметры сессии, как если бы они были атрибутами:
userId - ID текущего зарегистрированного или
замещенного пользователя;
userLogin - логин текущего зарегистрированного или
замещенного пользователя в нижнем регистре.
Атрибуты реплицируются в кластере Middleware так же,
как и все остальные данные сессии.
5.2.11.2. 5.2.11.2. Вход в систему
Платформа предоставляет встроенные механизмы
аутентификации, функциональность которых может быть
расширена в приложениях. Они включают в себя
различные схемы аутентификации, такие как вход по
паролю, функциональность "Запомнить меня", доверенный
и анонимный вход в систему.
Платформа включает следующие механизмы среднего
слоя:
AuthenticationManager реализованный классом
AuthenticationManagerBean
Реализации интерфейса AuthenticationProvider
AuthenticationService реализованный классом
AuthenticationServiceBean
UserSessionLog - см. журналирование пользовательских
сессий.
Рисунок 11. 11. Механизмы аутентификации среднего слоя
Также, платформа включает следующие дополнительные
компоненты:
TrustedClientService реализованный классом
TrustedClientServiceBean - предоставляет
анонимную/системную сессию для доверенных
приложений-клиентов.
AnonymousSessionHolder - создаёт и хранит анонимную
сессю для доверенных приложений-клиентов.
UserCredentialsChecker - проверяет, могут ли быть
использованы переданные Credentials, например, для
защиты от атак типа brute-force.
UserAccessChecker - проверяет, может ли пользователь
выполнять вход из данного контекста, например, в REST
API или с указанного IP адреса.
API или с указанного IP адреса.
Основной интерфейс аутентификации -
AuthenticationManager, включающий 4 метода:
Здесь есть два метода с похожей ответственностью:
authentucate() и login(). Оба метода проверяют,
являются ли переданные аутентификационные данные
валидными и соответствуют ли они активному
пользователю системы, затем возвращают объект
AuthenticationDetails. Основное отличие в работе этих
методов в том, что метод login() активирует сессию
пользователя, так что она может использоваться в
дальнейшем для вызова сервисов.
Объект Credentials представляет собой набор
аутентификационных данных. Платформа поставляет
несколько типов аутентификационных данных:
Доступные на всех слоях:
LoginPasswordCredentials
RememberMeCredentials
TrustedClientCredentials
public interface AuthenticationManager {
AuthenticationDetails authenticate(Credentials
credentials) throws LoginException;
AuthenticationDetails login(Credentials
credentials) throws LoginException;
UserSession substituteUser(User substitutedUser);
void logout();
}
TrustedClientCredentials
Доступные только на среднем слое:
SystemUserCredentials
AnonymousUserCredentials
Методы login / authenticate AuthenticationManager
возвращают объект AuthenticationDetails, который
содержит объект UserSession. Этот объект может быть
использован для проверки дополнительных разрешений,
чтения свойств объекта UserUser и атрибутов сессии. В
платформе есть встроенная реализация интерфейса
AuthenticationDetails - SimpleAuthenticationDetailsSimpleAuthenticationDetails, который
хранит только объект сессии пользователя, приложения
могут предоставлять свою реализацию
AuthenticationDetails с дополнительной инфомацией
для приложений-клиентов.
AuthenticationManagerAuthenticationManager может выполнить метод authenticate()authenticate() с
одним из следующих результатов:
вернуть объект AuthenticationDetails если он
подтверждает, что переданные аутентификационные
данные верны и соответствуют активному
пользователю системы.
выбросить LoginException если невозможно
аутентифицировать пользователя, неверны
аутентификационные данные или если пользователю
запрещён доступ к системе.
выбросить UnsupportedCredentialsException если
переданный тип аутентификационных данных не
поддерживается системой.
Реализация AuthenticationManager по умолчанию -
AuthenticationManagerBean, который делегирует
аутентификацию цепочке экземпляров
AuthenticationProvider. AuthenticationProvider - это
модуль аутентификации, который может обрабатывать
модуль аутентификации, который может обрабатывать
объекты Credentials определённого типа.
AuthenticationProvider также имеет специальный метод
supports() позволяющий узнать поддерживается ли
переданный тип аутентификационных данных.
Рисунок 12. 12. Стандартный процесс входа пользователя
Стандартный процесс входа пользователя:
пользователь вводит свой логин и пароль
клиентский блок приложения хэширует пароль,
вызывая метод getPlainHash() бина
PasswordEncryption и вызывает на Middleware метод
LoginService.login(), передавая ему логин
пользователя и хэш пароля.
Connection создаёт объект Credentials и вызывает
метод login() сервиса AuthenticationService.
AuthenticationService делегирует выполнение бину
AuthenticationManager, который использует цепочку
объектов AuthenticationProvider. В этой цепочке
имеется бин LoginPasswordAuthenticationProvider,
поддерживающий аутентификационные данные типа
LoginPasswordCredentials. Он загружает объект User
по полученному логину, хэширует полученный хэш
пароля повторно, используя в качестве соли
идентификатор пользователя, и сравнивает
полученный хэш с сохраненным в БД хэшем пароля. В
случае несовпадения выбрасывается исключение
LoginException.
После успешной аутентификации в созданный
экземпляр UserSession загружаются все параметры
доступа данного пользователя: список ролей, права,
ограничения и атрибуты сессии.
Если журналирование пользовательских сессий
активировано, в базу данных сохраняется запись с
информацией о текущей сессии.
Алгоритм хэширования паролей реализуется бином типа
EncryptionModule и задается в свойстве приложения
cuba.passwordEncryptionModule. По умолчанию - SHA-1.
Встроенные провайдеры аутентификации
Платформа включает следующие реализации
интерфейса AuthenticationProvider:
LoginPasswordAuthenticationProvider
RememberMeAuthenticationProvider
TrustedClientAuthenticationProvider
SystemAuthenticationProvider
AnonymousAuthenticationProvider
Все реализации загружают пользователя из базы
данных, проверяют переданные аутентификационные
данные и создают неактивный экземпляр
пользовательской сессии при помощи
UserSessionManager. Этот экземпляр может стать
активным позднее, если вызывается метод
AuthenticationManager.login().
Бины LoginPasswordAuthenticationProvider,
Бины LoginPasswordAuthenticationProvider,
RememberMeAuthenticationProvider и
TrustedClientAuthenticationProvider используют
дополнительные подключаемые проверки: бины,
реализующие интерфейс UserAccessChecker. Если по
крайней мере одна из таких проверок выбрасывает
исключение LoginException, то аутентификация не
выполняется и исключение LoginException передаётся
вызывающему коду.
Кроме того, LoginPasswordAuthenticationProvider и
RememberMeAuthenticationProvider проверяют
аутентификационные данные при помощи бинов
UserCredentialsCheckerUserCredentialsChecker. Имеется одна встроенная
реализация этого интерфейса -
BruteForceUserCredentialsCheckerBruteForceUserCredentialsChecker, который проверяет,
пытается ли пользователь подобрать верные
аутентификационные данные при помощи атаки brute-
force.
Исключения
AuthenticationManager и AuthenticationProvider могут
выбрасывать исключение LoginExceptionLoginException или одного из его
наследников из методов authenticate() и login().
Исключение UnsupportedCredentialsExceptionUnsupportedCredentialsException
выбрасывается, если переданный объект
аутентификационных данных Credentials не может
быть обработан имеющимися AuthenticationProvider.
См. следующие классы исключений:
UnsupportedCredentialsException
LoginException
AccountLockedException
UserIpRestrictedException
RestApiAccessDeniedException
События
События
Стандартная реализация AuthenticationManager -
AuthenticationManagerBean публикует следующие
события во время аутентификации / входа
пользователей:
BeforeAuthenticationEvent /
AfterAuthenticationEvent
BeforeLoginEvent / AfterLoginEvent
AuthenticationSuccessEvent /
AuthenticationFailureEvent
UserLoggedInEvent / UserLoggedOutEvent
UserSubstitutedEvent
Бины Spring на среднем слое приложения могут
обрабатывать эти события при помощи механизма
подписок @EventListener:
Обработчики всех событий, перечисленных выше (кроме
AfterLoginEvent, UserSubstitutedEvent и
UserLoggedInEvent), могут выбросить LoginException,
чтобы прервать процесс аутентификации / входа
@Component
public class LoginEventListener {
@Inject
private Logger log;
@EventListener
private void onUserLoggedIn(UserLoggedInEvent
event) {
User user = event.getSource().getUser();
log.info("Logged in user {}",
user.getInstanceName());
}
}
чтобы прервать процесс аутентификации / входа
пользователя.
Например, мы можем реализовать механизм режима
обслуживания системы, который позволит запретить
вход в систему на время её обслуживания.
Точки расширения
Вы можете расширить механизм аутентификации,
используя следующие точки расширения:
AuthenticationService - заменить существующий
AuthenticationServiceBean.
@Component
public class MaintenanceModeValve {
private volatile boolean maintenance = true;
public boolean isMaintenance() {
return maintenance;
}
public void setMaintenance(boolean maintenance)
{
this.maintenance = maintenance;
}
@EventListener
private void onBeforeLogin(BeforeLoginEvent
event) throws LoginException {
if (maintenance && event.getCredentials()
instanceof AbstractClientCredentials) {
throw new LoginException("Sorry, system
is unavailable");
}
}
}
AuthenticationManager - заменить существующий
AuthenticationManagerBean.
AuthenticationProvider - реализовать новый или
заменить существующий бин AuthenticationProvider.
Events - реализовать обработчик одного из доступных
событий.
Вы можете заменить существующие бины, задействуя
механизмы Spring Framework, например, зарегистрировав
новую реализацию в XML конфигурации Spring модуля
corecore:
<bean id="cuba_LoginPasswordAuthenticationProvider"
class="com.company.authext.core.CustomLoginPasswordA
uthenticationProvider"/>
public class
CustomLoginPasswordAuthenticationProvider extends
LoginPasswordAuthenticationProvider {
@Inject
public
CustomLoginPasswordAuthenticationProvider(Persistence
persistence, Messages messages) {
super(persistence, messages);
}
@Override
public AuthenticationDetails
authenticate(Credentials credentials) throws
LoginException {
LoginPasswordCredentials loginPassword =
(LoginPasswordCredentials) credentials;
// for instance, add new check before login
if ("demo".equals(loginPassword.getLogin()))
Обработчики событий могут быть упорядочены при
помощи аннотации @Order. Все бины и обработчики
событий платформы используют значение order из
диапазона 100 и 1000, что позволяет добавлять
обработчики на уровне проект как до, так и после кода
платформы. Если вы хотите добавить свой обработчик
до обработчиков/бинов платформы - используйте
значение меньше 100.
Задание order для обработчика события:
Бины AuthenticationProvider могут реализовать
интерфейс OrderedOrdered и метод getOrder() для определения
if ("demo".equals(loginPassword.getLogin()))
{
throw new LoginException("Demo account
is disabled");
}
return super.authenticate(credentials);
}
}
@Component
public class DemoEventListener {
@Inject
private Logger log;
@Order(10)
@EventListener
private void onUserLoggedIn(UserLoggedInEvent
event) {
log.info("Demo");
}
}
интерфейс OrderedOrdered и метод getOrder() для определения
очерёдности исполнения.
@Component
public class DemoAuthenticationProvider extends
AbstractAuthenticationProvider
implements AuthenticationProvider, Ordered {
@Inject
private UserSessionManager userSessionManager;
@Inject
public DemoAuthenticationProvider(Persistence
persistence, Messages messages) {
super(persistence, messages);
}
@Nullable
@Override
public AuthenticationDetails
authenticate(Credentials credentials) throws
LoginException {
// ...
}
@Override
public boolean supports(Class<?>
credentialsClass) {
return
LoginPasswordCredentials.class.isAssignableFrom(crede
ntialsClass);
}
@Override
public int getOrder() {
return 10;
}
}
Дополнительные возможности
В платформе имеется механизм защиты от взлома
пароля методом перебора. Для его включения
необходимо установить свойство приложения
cuba.bruteForceProtection.enabled для блока Middleware. В
этом случае после определенного количества
неуспешных попыток входа для определенного имени
пользователя с определенного IP-адреса вход для
пары логин + IP-адрес блокируется на некоторое
время. Допустимое количество попыток входа для
пары логин + IP-адрес определяется свойством
приложения
cuba.bruteForceProtection.maxLoginAttemptsNumber (по
умолчанию 5). Интервал блокировки пользователя в
секундах задается свойством
cuba.bruteForceProtection.blockIntervalSec (по умолчанию
60).
Возможен вариант, когда пароль пользователя
(точнее, хэш пароля) не хранится в базе данных, а
проверяется внешними средствами, например, путем
интеграции с ActiveDirectoryActiveDirectory. В этом случае фактически
аутентификацию выполняет клиентский блок, а
Middleware "доверяет" клиенту, создавая сессию по
одному только логину пользователя без пароля
методом LoginService.loginTrusted(). Метод
loginTrusted() требует выполнения следующих
условия:
клиентский блок должен передать так называемый
доверенный пароль, задаваемый на Middleware и на
клиентском блоке свойством приложения
cuba.trustedClientPassword
IP-адрес клиентского блока должен быть в списке,
}
задаваемой свойством приложения
cuba.trustedClientPermittedIpList
Вход в систему требуется также для автоматических
процессов, запускаемых по расписанию, а также при
подключении к бинам Middleware через JMX-интерфейс.
Строго говоря, такие действия считаются
административными и не требуют аутентификации до
тех пор, пока не выполняется каких-либо изменений
сущностей в базе данных. При записи сущностей в БД
требуется проставить логин пользователя, который
выполнил изменения, поэтому для работы таких
процессов должен быть указан пользователь, от лица
которого выполняются изменения.
Дополнительным плюсом входа в систему для
автоматического процесса и для JMX-вызова является
то, что вывод в журнал сообщений от логгеров
сопровождается указанием логина текущего
пользователя, если пользовательская сессия
установлена в потоке выполнения. Это упрощает
поиск сообщений от конкретного процесса при
разборе журнала.
Вход в систему для процессов внутри Middleware
выполняется вызовом
AuthenticationManager.loginSystem() с передачей
объекта SystemUserCredentials , содержащего логин
пользователя (без пароля), от имени которого будет
работать данный процесс. В результате создается
объект UserSession, который будет закэширован в
данном блоке Middleware и не будет реплицироваться в
кластере.
Более подробно аутентификация процессов внутри
Middleware рассмотрена в разделе Системная
аутентификация.
Устаревшие механизмы
Следующие компоненты считаются устаревшими:
LoginService делегирует исполнение методов
сервису AuthenticationService
LoginWorker делегирует исполнение методов бину
AuthenticationManager
Не используйте эти компоненты в коде приложений. Они
будут удалены в одной из следующих версий
платформы.
5.2.11.3. SecurityContext5.2.11.3. SecurityContext
Экземпляр класса SecurityContext хранит информацию о
пользовательской сессии для текущего потока
выполнения. Он создается и передается в метод
AppContext.setSecurityContext() в следующие
моменты:
для блоков Web Client и Web Portal - в начале обработки
каждого HTTP-запроса от пользовательского браузера.
для блока Middleware - в начале обработки каждого
запроса от клиентского уровня и от назначенных
заданий CUBA.
для блока Desktop Client - один раз после входа
пользователя, так как десктопное приложение является
однопользовательским.
По окончании выполнения запроса в первых двух случаях
SecurityContext удаляется из потока выполнения.
При создании прикладным кодом нового потока
выполнения в него необходимо передать текущий
экземпляр SecurityContext, например:
final SecurityContext securityContext =
То же самое можно сделать, используя обертки
SecurityContextAwareRunnable или
SecurityContextAwareCallable, например:
5.2.12. 5.2.12. Обработка исключений
В данном разделе рассмотрены различные аспекты
генерации и обработки исключений в CUBA-приложениях.
5.2.12.1. 5.2.12.1. Классы исключений
При создании собственных классов исключений следует
придерживаться следующих правил:
final SecurityContext securityContext =
AppContext.getSecurityContext();
executor.submit(new Runnable() {
public void run() {
AppContext.setSecurityContext(securityContext);
// business logic here
}
});
executor.submit(new SecurityContextAwareRunnable<>(()
-> {
// business logic here
}));
Future<String> future = executor.submit(new
SecurityContextAwareCallable<>(() -> {
// business logic here
return some_string;
}));
придерживаться следующих правил:
Если исключение является нормальной частью бизнес-
логики и при его возникновении требуется
предпринимать некоторые нетривиальные действия, то
класс исключения следует делать декларируемым
(наследником Exception). Обработка таких исключений
производится вызывающим кодом.
Если исключение сигнализирует об ошибочной
ситуации, и реакцией на него должно быть прерывание
хода выполнения и простое действие типа отображения
информации об ошибке пользователю, то класс
исключения следует делать недекларируемым
(наследником RuntimeException). Обработка таких
исключений производится специальными классами-
обработчиками, зарегистрированными в клиентских
блоках приложения.
Если исключение выбрасывается и обрабатывается в
рамках одного блока приложения, то класс исключения
следует объявлять в соответствующем модуле. Если же
исключение выбрасывается на Middleware, а
обрабатывается на клиентском уровне, то класс
исключения необходимо объявлять в модуле globalglobal.
Платформа содержит специальный класс
недекларируемого исключения SilentException, который
можно использовать для прерывания хода выполнения
без выдачи каких-либо сообщений пользователю или в
лог. SilentException объявлен в модуле globalglobal, поэтому
доступен как на Middleware, так и в клиентских блоках.
5.2.12.2. 5.2.12.2. Передача исключений
MiddlewareMiddleware
Если при выполнении запроса от клиента на Middleware
возникает исключение, выполнение прерывается и на
клиента возвращается объект исключения, как правило,
включающий цепочку порождающих друг друга
исключений. Так как цепочка исключений может
содержать классы, недоступные клиентскому блоку
(например, исключения JDBC-драйвера), на клиента
передается не сама эта цепочка, а ее представление
внутри специального создаваемого исключения
RemoteException.
Информация об исключениях-причинах сохраняется в
виде списка объектов RemoteException.Cause. Каждый
объект Cause хранит обязательно имя класса исключения
и его сообщение. Кроме того, если класс исключения
"поддерживается клиентом", то Cause содержит также и
сам объект исключения. Это дает возможность передать
на клиента информацию в полях исключения.
Класс исключения, объекты которого нужно передавать
на клиентский уровень именно в виде Java-объектов,
нужно аннотировать @SupportedByClient, например:
Таким образом, при возникновении на Middleware
исключения, не аннотированного @SupportedByClient,
вызывающий клиентский код получит RemoteException,
внутри которого будет находиться исходное исключение
в виде строки. Если же исходное исключение
аннотировано @SupportedByClient, то вызывающий код
получит именно его. Это дает возможность в прикладном
коде организовывать обработку декларируемых
@SupportedByClient
public class WorkflowException extends
RuntimeException {
...
сервисами Middleware исключений традиционным образом -
с помощью блоков try/catch.
Следует иметь в виду, что чтобы поддерживаемое
клиентом исключение было действительно передано на
клиента в виде объекта, оно не должно содержать внутри
себя в цепочке getCause() неподдерживаемых
исключений. Поэтому если вы создаете на Middleware
экземпляр исключения и хотите передать его на клиента,
указывайте для него параметр cause только если вы
уверены, что он содержит только исключения, известные
клиенту.
Упаковку объектов исключений в RemoteException перед
передачей на клиентский уровень выполняет
перехватчик вызовов сервисов - класс
ServiceInterceptor. Кроме того, он же выполняет
логирование исключений. По умолчанию в журнал
выводится вся информация об исключении, включая
полный stack trace. Если это нежелательно, можно
добавить классу исключения аннотацию @Logging, указав
в ней тип логирования:
FULL - (по умолчанию) полная информация, включая
stacktrace
BRIEF - только имя класса исключения и сообщение
NONE - не выводить ничего
Например:
@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends
Exception {
...
5.2.12.3. 5.2.12.3. Обработчики исключений
клиентского уровня
Необработанные исключения в блоках Web Client и Desktop
Client, возникшие на клиентском уровне или переданные с
Middleware, попадают в специальный механизм
обработчиков. Этот механизм реализован в модуле GUI и
доступен обоим клиентам.
Обработчик должен быть управляемым бином,
реализовывать интерфейс GenericExceptionHandler, в
методе handle() которого производить обработку и
возвращать true, либо сразу возвращать false, если
данный обработчик не может обработать переданное ему
исключение. Такое поведение позволяет организовать
"цепочку ответственности" обработчиков.
Рекомендуется наследовать классы своих обработчиков
от базового класса AbstractGenericExceptionHandler,
который умеет разбирать цепочку исключений (с учетом
упакованных внутри RemoteException) и реагировать на
конкретные типы исключений. Типы исключений, для
которых предназначен данный обработчик, указываются в
массиве строк, передаваемом в конструкторе
обработчика базовому конструктору. Каждая строка
массива должна содержать одно полное имя класса
обрабатываемого исключения, например:
@Component("cuba_EntityAccessExceptionHandler")
public class EntityAccessExceptionHandler extends
AbstractGenericExceptionHandler {
public EntityAccessExceptionHandler() {
super(EntityAccessException.class.getName());
}
Если класс исключения недоступен на клиенте, следует
указывать его имя строковым литералом:
В случае использования в качестве базового класса
AbstractGenericExceptionHandler логика обработки
располагается в методе doHandle(), и может выглядеть
следующим образом:
Если имени класса исключения недостаточно для того,
чтобы принять решение о применимости данного
обработчика к исключению, следует определить метод
}
...
@Component("cuba_OptimisticExceptionHandler")
public class OptimisticExceptionHandler extends
AbstractGenericExceptionHandler implements Ordered {
public OptimisticExceptionHandler() {
super("javax.persistence.OptimisticLockException");
}
...
@Override
protected void doHandle(String className, String
message, @Nullable Throwable throwable, WindowManager
windowManager) {
String msg =
messages.getMainMessage("zeroBalance.message");
windowManager.showNotification(msg,
IFrame.NotificationType.ERROR);
}
обработчика к исключению, следует определить метод
canHandle(), получающий кроме прочего текст
исключения. Метод должен вернуть true, если данный
обработчик применим для исключения. Например:
WindowManager предоставляет метод
showExceptionDialog() для отображения диалогового
окна с выброшенным исключением и его stacktrace. Метод
принимает следующие параметры:
throwable - экземпляр Throwable.
caption - заголовок диалога. Это необязательный
параметр, если он не установлен, используется
заголовок по умолчанию.
message - сообщение диалога. Это необязательный
параметр, если он не установлен, используется
сообщение по умолчанию.
Пример вызова диалога в обработчике исключения:
@Component("cuba_NumericOverflowExceptionHandler")
public class NumericOverflowExceptionHandler extends
AbstractGenericExceptionHandler {
public NumericOverflowExceptionHandler() {
super(EclipseLinkException.class.getName());
}
@Override
protected boolean canHandle(String className,
String message, @Nullable Throwable throwable) {
return StringUtils.containsIgnoreCase(message,
"Numeric field overflow");
}
...
5.2.13. Bean Validation5.2.13. Bean Validation
Bean Validation - опциональный механизм, обеспечивающий
единообразную валидацию данных на среднем слое, в
Generic UI и в REST API. Он основан на спецификации JSR 349
- Bean Validation 1.1 и ее референсной имплементации:
Hibernate Validator.
5.2.13.1. 5.2.13.1. Задание ограничений
Ограничения bean validation задаются с помощью
аннотаций пакета javax.validation.constraints или
собственных аннотаций. Аннотации указываются на
декларации класса сущности или POJO, на поле или getter-
методе, а также на методе сервиса middleware.
Пример использования стандартных аннотаций
валидации на полях сущности:
@Override
protected void doHandle(String className, String
message, @Nullable Throwable throwable, WindowManager
windowManager) {
if (throwable != null)
windowManager.showExceptionDialog(throwable,
"Exception is thrown", message);
else
windowManager.showNotification(message,
IFrame.NotificationType.ERROR);
}
@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo$Customer")
public class Customer extends StandardEntity {
Пример использования собственной аннотации уровня
класса (см. ниже):
Пример валидации параметров и возвращаемого
значения метода сервиса:
@Size(min = 3)
// length of value must be longer
then 3 characters
@Column(name = "NAME", nullable = false)
protected String name;
@Min(1)
// minimum value
@Max(5)
// maximum value
@Column(name = "GRADE", nullable = false)
protected Integer grade;
@Pattern(regexp = "\\S+@\\S+")
// value must
conform to the pattern
@Column(name = "EMAIL")
protected String email;
//...
}
@CheckTaskFeasibility(groups = {Default.class,
UiCrossFieldChecks.class})
// custom validation
annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
//...
}
public interface TaskService {
String NAME = "demo_TaskService";
Группы ограничений
Группы ограничений позволяют применять
подмножество всех заданных ограничений в
зависисмости от логики приложения. Например, вы
можете заставить пользователя ввести значение
некоторого атрибута сущности в UI, и а то же время
иметь возможность установить данный атрибут в null в
некотором внутреннем механизме. Для этого
необходимо указать атрибут groups в аннотации
ограничения, и оно будет действовать только когда эта
же группа передается в механизм валидации.
Платформа передает в механизм валидации следующие
группы ограничений:
RestApiChecks - при валидации в REST API.
ServiceParametersChecks - при валидации параметров
сервисов.
ServiceResultChecks - при валидации возвращаемых
значений сервисов.
UiComponentChecks - при валидации отдельных полей в
UI.
UiCrossFieldChecks - при валидации ограничений
уровня класса на коммите экрана редактора
сущности.
javax.validation.groups.Default - данная группа
String NAME = "demo_TaskService";
@Validated
// indicates that the method should be
validated
@NotNull
String completeTask(@Size(min = 5) String comment,
@NotNull Task task);
}
передается во всех случаях кроме коммита экрана
редактора сущности.
Сообщения валидации
Ограничения могут иметь сообщения для отображения
пользователям.
Сообщения могут быть указаны непосредственно в
аннотациях валидации, например:
Сообщения можно также поместить в пакет
локализованных сообщений и использовать следующий
формат указания сообщения в аннотации:
{msg://message_pack/message_key}. Например:
Сообщения могут содержать параметры и выражения.
Параметры заключаются в фигурные скобки {} и
представляют собой либо указатели на локализованные
сообщения (см. выше) или параметры аннотации,
например {min}, {max}, {value}. Выражения
заключаются в фигурные скобки со знаком доллара ${}
и могут включать валидируемое значение в виде
переменной validatedValue, параметры аннотации типа
value или min, и выражения JSR-341 (EL 3.0). Например:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid
format")
@Column(name = "EMAIL")
protected String email;
@Pattern(regexp = "\\S+@\\S+", message =
"{msg://com.company.demo.entity/Customer.email.valida
tionMsg}")
@Column(name = "EMAIL")
protected String email;
value или min, и выражения JSR-341 (EL 3.0). Например:
Значения локализованных сообщений также могут
содержать параметры и выражения.
Собственные ограничения
В проекте можно создать собственные ограничения с
программной или декларативной валидацией.
Для создания ограничения с программной валидацией
выполниет следующее:
1. Создайте аннотацию в модуле globalglobal проекта и
добавьте ей аннотацию @Constraint. Ваша аннотация
должна содержать атрибуты message, groups и
payload:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid
email: ${validatedValue}, pattern: {regexp}")
@Column(name = "EMAIL")
protected String email;
@Target({ ElementType.TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy =
TaskFeasibilityValidator.class)
public @interface CheckTaskFeasibility {
String message() default
"{msg://com.company.demo.entity/CheckTaskFeasibili
ty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default
{};
}
2. Создайте класс валидатора в модуле globalglobal проекта:
3. Используйте аннотацию:
public class TaskFeasibilityValidator implements
ConstraintValidator<CheckTaskFeasibility, Task> {
@Override
public void initialize(CheckTaskFeasibility
constraintAnnotation) {
}
@Override
public boolean isValid(Task value,
ConstraintValidatorContext context) {
Date now =
AppBeans.get(TimeSource.class).currentTimestamp();
return !
(value.getDueDate().before(DateUtils.addDays(now,
3)) && value.getProgress() < 90);
}
}
@CheckTaskFeasibility(groups =
UiCrossFieldChecks.class)
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
@Future
@Temporal(TemporalType.DATE)
@Column(name = "DUE_DATE")
protected Date dueDate;
Собственные аннотации могут также быть созданы как
композиции имеющихся, например:
При использовании композитных ограничений
результирующий набор нарушений
ConstraintViolation будет содержать отдельные
записи для каждого включенного ограничения. Для того,
чтобы получить одну запись нарушения, добавьте
@ReportAsSingleViolation классу вашей аннотации.
Валидация связанных объектов
Для каскадной валидации связанных объектов на
@Min(0)
@Max(100)
@Column(name = "PROGRESS", nullable = false)
protected Integer progress;
//...
}
@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidProductCode {
String message() default
"{msg://om.company.demo.entity/ValidProductCode.messa
ge}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Для каскадной валидации связанных объектов на
ссылочные поля необходимо добавить аннотацию
@Valid:
В примере выше, при валидации экземпляра Cart список
продуктов будет проверен на наличие хотя-бы одной
записи, и все экземпляры Product в списке будут также
провалидированы.
Аннотации валидации, , заданные в CUBA CUBA
Кроме стандартных аннотаций из пакета
javax.validation.constraints можно использовать
следующую аннотацию, определенную в платформе:
@RequiredView - может быть добавлена на методы
сервисов для того, чтобы во время выполнения
убедиться, что в сущностях загружены все атрибуты,
заданные в требуемых представлениях. Если
аннотация задана на методе, то проверяется
результат метода. Если аннотация задана на
параметре, проверяется этот параметр. Если параметр
или результат являются коллекцией, то проверяются
все элементы этой коллекции. Пример использования:
public class Cart {
@Size(min = 1)
@Valid
private List<Product> products;
// ...
}
public interface MyService {
String NAME = "sample_MyService";
5.2.13.2. 5.2.13.2. Запуск валидации
Валидация в UI UI
Компоненты Generic UI, соединенные с источником
данных, получают экземпляр BeanValidator для
проверки значения. Валидатор вызывается из метода
Component.Validatable.validate(), реализуемого
компонентом, и может выбрасывать исключение
CompositeValidationException, содержащее набор
объектов нарушений.
Стандартный валидатор может быть программно удален
или проинициализирован другой группой ограничений:
@Validated
void processFoo(@RequiredView("foo-view") Foo
foo);
@Validated
void processFooList(@RequiredView("foo-view")
List<Foo> fooList);
@Validated
@RequiredView("bar-view")
Bar loadBar(@RequiredView("foo-view") Foo foo);
}
public class Screen extends AbstractWindow {
@Inject
private TextField field1;
@Inject
private TextField field2;
По умолчанию, BeanValidator содержит две группы:
Default и UiComponentChecks.
Если атрибут сущности аннотирован @NotNull без
группы ограничений, он будет помечен как
обязательный в метаданных, и UI-компоненты
работающие с данным атрибутом через источник
данных, будут иметь свойство required = true.
Компоненты DateField и DatePicker автоматически
устанавливают свои свойства rangeStart и rangeEnd в
соответствии с аннотациями @Past и @Future, не
учитывая текущее время.
public void init(Map<String, Object> params) {
// Completely remove bean validation from
the UI component
field1.getValidators().stream()
.filter(validator -> validator
instanceof BeanValidator)
.forEach(textField::removeValidator);
// Here validators will check only
constraints with explicitly set UiComponentChecks
group, because
// the Default group will not be passed
field2.getValidators().stream()
.filter(validator -> validator
instanceof BeanValidator)
.forEach(validator -> {
((BeanValidator)
validator).setValidationGroups(new Class[]
{UiComponentChecks.class});
});
}
}
учитывая текущее время.
Экраны редактирования выполняют валидацию
ограничений уровня класса при коммите, если
ограничения включают группу UiCrossFieldChecks и все
проверки ограничений уровня атрибутов прошли
успешно. Валидацию данного типа можно отключить с
помощью свойства экрана crossFieldValidate в XML-
дескрипторе или в контроллере:
Валидация сервисов Middleware Middleware
Сервисы среднего слоя выполняют валидацию
параметров и результатов методов, если метод имеет
аннотацию @Validated в интерфейсе сервиса. Например:
<window
xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editorCaption"
class="com.company.demo.web.task.TaskEdit"
datasource="taskDs"
crossFieldValidate="false">
<!-- ... -->
</window>
public class TaskEdit extends AbstractEditor<Task> {
@Override
public void init(Map<String, Object> params) {
setCrossFieldValidate(false);
}
}
public interface TaskService {
String NAME = "demo_TaskService";
@Validated
Аннотация @Validated может содержать указание групп
ограничений для применения подмножества имеющихся
ограничений. Если группы не указаны, то по умолчанию
используются следующие:
Default и ServiceParametersChecks - для параметров
методов
Default и ServiceResultChecks - для возвращаемых
результатов методов
При возникновении ошибок валидации выбрасываются
исключения MethodParametersValidationException и
MethodResultValidationException.
При выполнении некоторой специфической
программной валидации в сервисе, можно использовать
исключение CustomValidationException для того, чтобы
клиенты получали информацию об ошибках в том же
формате что и от стандартной валидации. Это может
быть особенно актуально для клиентов REST API.
Валидация в REST API REST API
Универсальный REST API автоматически выполняет bean
validation для действий создания и изменения сущностей.
Ошибки валидации передаются клиенту следующим
образом:
MethodResultValidationException и
ValidationException порождают HTTP статус 500
Server error
MethodParametersValidationException,
ConstraintViolationException и
@Validated
@NotNull
String completeTask(@Size(min = 5) String
comment, @NotNull Task task);
}
ConstraintViolationException и
CustomValidationException порождают HTTP статус
400 Bad request
Тело ответа с Content-Type: application/json будет
содержать список объектов со свойствами message,
messageTemplate, path и invalidValue, например:
path содержит путь к невалидному атрибуту в
валидируемом графе объектов
messageTemplate содержит сторку, заданную в
атрибуте message аннотации
message содержит сообщение ошибки валидации
invalidValue возвращается только если тип значения
один из следующих: String, Date, Number, Enum, UUID.
Программная валидация
Bean validation можно запустить программно используя
интерфейс инфраструктуры BeanValidation, доступный
и на middleware и на клиентском уровне. Через данный
интерфейс необходимо получить реализацию
javax.validation.Validator, которая и запускает
валидацию. Результатом валидации является набор
объектов типа ConstraintViolation. Например:
[
{
"message": "Invalid email: aaa",
"messageTemplate":
"{msg://com.company.demo.entity/Customer.email.vali
dationMsg}",
"path": "email",
"invalidValue": "aaa"
}
]
5.2.14. 5.2.14. Контроль доступа к атрибутам сущностей
Подсистема безопасности позволяет управлять доступом
к атрибутам сущностей в соответствии с правами
пользователя. Т.е. фреймворк может сделать атрибут
read-only или скрыть его в зависимости от набора ролей,
назначенных текущему пользователю. Однако, иногда
может потребоваться изменять доступ к атрибуту в
зависимости также от текущего состояния экземпляра
сущности или связанных сущностей.
Механизм контроля доступа к атрибутам позволяет
создавать правила того, какие атрибуты должны быть
скрыты, нередактируемы или обязательны к заполнению
для некоторого экземпляра сущности, и применять эти
правила к компонентам Generic UI и в REST API.
Данный механизм работает следующим образом:
Когда DataManager загружает сущность, он посылает
Spring application event типа SetupAttributeAccessEvent.
Объект события содержит загруженный экземпляр в
состоянии managed и три коллекции имен атрибутов:
read-only, hidden и required (изначально они пустые).
Вам необходимо создать event listener, который будет
@Inject
private BeanValidation beanValidation;
public void save(Foo foo) {
Validator validator =
beanValidation.getValidator();
Set<ConstraintViolation<Foo>> violations =
validator.validate(foo);
// ...
}
Вам необходимо создать event listener, который будет
анализировать состояние сущности и заполнять списки
имен атрибутов соответствующим образом. Данный
event listener является по сути контейнером правил,
задающих доступ к атрибутам экземпляров.
Описываемый механизм сохраняет имена атрибутов,
заданные правилами, в самом экземпляре (в связанном
объекте SecurityState).
На клиентском уровне, Generic UI и REST API используют
объект SecurityState для управления доступом к
атрибутам сущностей.
Для создания правил для некоторого типа сущностей,
необходимо выполнить следующее:
Создайте управляемый бин в модуле corecore проекта. Бин
должен иметь дефолтный singleton scope.
Создайте метод, принимающий единственный параметр
типа SetupAttributeAccessEvent. Метод должен иметь
аннотацию
org.springframework.context.event.EventListener.
Тип SetupAttributeAccessEvent является generic и
должен быть параметризован типом сущности.
В данном методе необходимо заполнить коллекции
скрываемых, только для чтения и обязательных
атрибутов используя методы addHidden(),
addReadOnly() и addRequired() объекта события.
Экземпляр сущности, доступный через метод
getEntity(), находится в состоянии managed, поэтому
можно безопасно обращаться ко всем его атрибутам и
атрибутам связанных сущностей.
Рассмотрим пример сущности Order, имеющей атрибуты
customer и amount. Для ограничения доступа к атрибуту
amount в зависимости от customer можно создать
следующее правило:
Контроль доступа к атрибутам в Generic UI Generic UI
Фреймворк автоматически применяет ограничения
доступа к экрану перед вызовом метода ready() его
контроллера. Если вы не хотите этого для некоторого
package com.company.sample.core;
import com.company.sample.entity.Order;
import
com.haulmont.cuba.core.app.events.SetupAttributeAccess
Event;
import
org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component("sample_AttributeAccessRules")
public class AttributeAccessRules {
@EventListener
public void
orderAccess(SetupAttributeAccessEvent<Order> event) {
Order order = event.getEntity();
if (order.getCustomer() != null) {
if
("PLATINUM".equals(order.getCustomer().getGrade().getC
ode())) {
event.addHidden("amount");
} else if
("GOLD".equals(order.getCustomer().getGrade().getCode(
))) {
event.addReadOnly("amount");
}
}
}
}
контроллера. Если вы не хотите этого для некоторого
экрана, переопределите метод
isAttributeAccessControlEnabled() его контроллера и
верните из него false.
Пересчитать и применить ограничения к экрану можно и
когда он уже открыт, в ответ на действия пользователя.
Для этого необходимо использовать бин
AttributeAccessSupport, передавая ему текущий экран
и сущность, состояние которой изменилось. Например:
package com.company.sample.web.order;
import com.company.sample.entity.Order;
import com.haulmont.cuba.gui.AttributeAccessSupport;
import
com.haulmont.cuba.gui.components.AbstractEditor;
import com.haulmont.cuba.gui.data.Datasource;
import javax.inject.Inject;
import java.util.Map;
public class OrderEdit extends AbstractEditor<Order>
{
@Inject
private Datasource<Order> orderDs;
@Inject
private AttributeAccessSupport
attributeAccessSupport;
@Override
public void init(Map<String, Object> params) {
orderDs.addItemPropertyChangeListener(e -> {
if ("customer".equals(e.getProperty()))
{
Второй параметр метода applyAttributeAccess() -
булевское значение, которое указывает, нужно ли
сбрасывать ограничения доступа к компонентам в
дефолтные настройки перед тем, как применить новые.
Если передано true, возможные программные изменения
в этих настройках будут потеряны. Когда данный метод
вызывается автоматически перед открытием окна,
передается false. Когда же вы вызываете данный метод в
ответ на UI-события, передавайте true, иначе ограничения
компонентов будут суммироваться, а не заменяться.
Ограничения доступа к атрибутам применяются
только к компонентам, связанным с одним
атрибутом сущности, например TextField или
LookupField. Table и другие компоненты, реализующие
интерфейс ListComponent, не затрагиваются.
Поэтому если вы пишете правило, скрывающее
атрибут для некоторых экземпляров,
рекомендуется не показывать этот атрибут в
таблицах совсем.
5.3. 5.3. Компоненты работы с базой данных
В данном разделе приведена информация о возможных
типах СУБД приложений на платформе CUBA. Кроме того,
описан механизм на основе скриптов, с помощью которого
можно создать новую базу данных, и в дальнейшем
attributeAccessSupport.applyAttributeAccess(this,
true, getItem());
}
});
}
}
можно создать новую базу данных, и в дальнейшем
поддерживать ее в актуальном состоянии на протяжении
всего цикла разработки и эксплуатации приложения.
Компоненты работы с базой данных принадлежат блоку
Middleware, другие блоки приложения не имеют прямого
доступа к БД.
Дополнительная практическая информация по работе с
базой данных приведена в секции ниже.
5.3.1. 5.3.1. Типы СУБД
Тип используемой СУБД определяется свойствами
приложения cuba.dbmsType и (опционально)
cuba.dbmsVersion, которые влияют на поведение
механизмов, зависящих от типа базы данных.
Приложение обращается к БД через источник данных
javax.sql.DataSource, который извлекается из JNDI по
имени, заданному в свойстве приложения
cuba.dataSourceJndiName (по умолчанию
java:comp/env/jdbc/CubaDS). Конфигурация источника
данных в случае стандартного развертывания задается в
файле context.xml модуля corecore. Источник данных должен
использовать JDBC-драйвер, соответствующий выбранной
СУБД.
Платформа "из коробки" поддерживает следующие СУБД:
cuba.dbmsTypecuba.dbmsType cuba.dbmsVersioncuba.dbmsVersion JDBC driverJDBC driver
HSQLDB hsql org.hsqldb.jdbc.JDBCDriver
PostgreSQL
8.4+
postgres org.postgresql.Driver
Microsoft
SQL Server
2005
mssql 2005 net.sourceforge.jtds.jdbc.Driver
Microsoft
SQL Server
2008
mssql com.microsoft.sqlserver.jdbc.SQLServerDriver
2008
Microsoft
SQL Server
2012+
mssql 2012 com.microsoft.sqlserver.jdbc.SQLServerDriver
Oracle
Database
11g+
oracle oracle.jdbc.OracleDriver
MySQL 5.6+ mysql com.mysql.jdbc.Driver
cuba.dbmsTypecuba.dbmsType cuba.dbmsVersioncuba.dbmsVersion JDBC driverJDBC driver
Таблица ниже описывает рекомендованное соответствие
типов данных между атрибутами сущностей в Java и
колонками таблиц различных СУБД. Эти типы
автоматически выбираются Studio при генерации скриптов
создания и обновления БД, и для них гарантируется
работоспособность всех механизмов платформы.
JavaJava HSQLHSQL PostgreSQLPostgreSQL MS SQL ServerMS SQL Server OracleOracle MySQLMySQL
UUID varchar(36) uuid uniqueidentier varchar2(32) varchar(32)
Date timestamp timestamp datetime timestamp datetime(3)
java.sql.Date timestamp date datetime date date
java.sql.Time timestamp time datetime timestamp time(3)
BigDecimal decimal(p, s) decimal(p, s) decimal(p, s) number(p, s) decimal(p, s)
Double double
precision
double
precision
double
precision
oat double
precision
Long bigint bigint bigint number(19) bigint
Integer integer integer integer integer integer
Boolean boolean boolean tinyint char(1) boolean
String
(limited)
varchar(n) varchar(n) varchar(n) varchar2(n) varchar(n)
String
(unlimited)
longvarchar text varchar(max) clob longtext
byte[] longvarbinary bytea image blob longblob
Как правило, всю работу по преобразованию данных
между БД и кодом Java выполняет слой ORM совместно с
соответствующим JDBC драйвером. Это означает, что при
работе с данными через методы EntityManager и запросы на
JPQL никакой ручной конвертации выполнять не нужно -
вы просто используете типы Java, перечисленные в левой
колонке таблицы.
При использовании native SQL через
EntityManager.createNativeQuery() или через QueryRunner для
разных типов СУБД некоторые типы данных в Java коде
будут отличаться от приведенных. В первую очередь это
касается атрибутов типа UUID - только драйвер PostgreSQL
возвращает значения соответствующих колонок в этом
типе, для других серверов это будет String. Для
обеспечения независимости кода от используемой СУБД
рекомендуется конвертировать типы параметров и
результатов запросов с помощью интерфейса
DbTypeConverter.
5.3.1.1. 5.3.1.1. Поддержка произвольных
СУБД
На уровне прикладного проекта можно реализовать
работу с любой СУБД, поддерживаемой фреймворком ORM
(EclipseLink). Для этого достаточно выполнить следующее:
Указать тип СУБД в виде произвольного кода в свойстве
cuba.dbmsType. Код должен отличаться от используемых
в платформе кодов hsql, postgres, mssql, oracle.
Реализовать интерфейсы DbmsFeatures,
SequenceSupport, DbTypeConverter классами с именами
соответственно TypeDbmsFeatures,
TypeSequenceSupport, TypeDbTypeConverter, где Type -
код типа СУБД. Пакет класса имплементации должен
быть таким же, как у интерфейса.
Создать скрипты инициализации и обновления БД в
Создать скрипты инициализации и обновления БД в
каталогах с кодом СУБД. Скрипты инициализации
должны включать создание всех объектов БД,
необходимых для сущностей платформы (их можно
скопировать из имеющихся в каталоге 10-cuba и др.
скриптов и исправить для данной СУБД).
Для создания и обновления БД задачами Gradle в
build.gradle необходимо для этих задач указать
дополнительные параметры:
task createDb(dependsOn: assemble, type:
CubaDbCreation) {
dbms = 'my'
// DBMS code
driver = 'net.my.jdbc.Driver'
// JDBC driver class
dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'
// Database URL
masterUrl =
'jdbc:my:myserver://192.168.47.45/master'
// URL of
a master DB to connect to for creating the
application DB
dropDbSql = 'drop database mydb;'
// Drop database statement
createDbSql = 'create database mydb;'
// Create database statement
timeStampType = 'datetime'
// Date and time datatype - needed for
SYS_DB_CHANGELOG table creation
dbUser = 'sa'
dbPassword = 'saPass1'
}
task updateDb(dependsOn: assemble, type:
CubaDbUpdate) {
dbms = 'my'
5.3.1.2. 5.3.1.2. Версия СУБД
В дополнение к свойству приложения cuba.dbmsType
существует опциональное свойство cuba.dbmsVersion. Оно
влияет на выбор имплементаций интерфейсов
DbmsFeatures, SequenceSupport, DbTypeConverter, и на
поиск скриптов создания и обновления БД.
Имя класса имплементации интеграционного интерфейса
формируется следующим образом: TypeVersionName.
Здесь Type - значение cuba.dbmsType с заглавной буквы,
Version - значение cuba.dbmsVersion, Name - имя
интерфейса. Пакет класса должен быть таким же, как у
интерфейса. Если класс с таким именем отсутствует,
предпринимается попытка найти класс с именем без
версии: TypeName. Если и такого класса нет, выдается
исключение.
Например, в платформе определен класс
com.haulmont.cuba.core.sys.persistence.Mssql2012Seq
uenceSupport, который вступит в силу, если в проекте
указаны следующие свойства:
dbms = 'my'
// DBMS code
driver = 'net.my.jdbc.Driver'
// JDBC driver class
dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'
// Database URL
dbUser = 'sa'
dbPassword = 'saPass1'
}
cuba.dbmsType = mssql
cuba.dbmsVersion = 2012
При поиске скриптов создания и обновления БД каталог с
именем type-version имеет приоритет над каталогом с
именем type. Это значит, что скрипты каталога type-
version заменяют одноименные скрипты каталога type. В
каталоге type-version могут быть и скрипты с
собственными именами, они будут также добавлены в
общий набор скриптов для выполнения. Сортировка
скриптов производится по пути начиная с каталога,
вложенного в type или type-version, то есть без учета
того, в каком каталоге (с версией или без) находится
скрипт.
Например, следующим образом можно определить
скрипты создания БД для Microsoft SQL Server для версий
ниже и выше 2012:
5.3.2. 5.3.2. Скрипты создания и обновления БД
Проект CUBA-приложения всегда содержит два набора
скриптов:
Скрипты
создания
БД, предназначенные для создания
базы данных с нуля. Они содержат набор DDL и DML
операторов, после выполнения которых на пустой БД
схема базы данных полностью соответствует текущему
состоянию модели данных приложения. Скрипты
cuba.dbmsVersion = 2012
modules/core/db/init/
mssql/
10.create-db.sql
20.create-db.sql
30.create-db.sql
mssql-2012/
10.create-db.sql
состоянию модели данных приложения. Скрипты
создания могут также наполнять БД необходимыми
первичными данными.
Скрипты
обновления
БД - предназначены для
поэтапного приведения структуры БД к текущему
состоянию модели данных.
При изменении модели данных необходимо отразить
соответствующее изменение схемы БД и в скриптах
создания, и в скриптах обновления. Например, при
добавлении атрибута address в сущность Customer,
нужно:
1. Изменить оператор создания таблицы в скрипте
создания:
2. Добавить скрипт обновления, содержащий оператор
модификации таблицы:
Скрипты создания располагаются в каталоге /db/init
модуля corecore. Для каждого типа СУБД, поддерживаемой
приложением, создается свой набор скриптов и
располагается в подкаталоге с именем, соответствующим
свойству приложения cuba.dbmsType, например,
/db/init/postgres. Имена скриптов создания должны
create table SALES_CUSTOMER (
ID varchar(36) not null,
CREATE_TS timestamp,
CREATED_BY varchar(50),
NAME varchar(100),
ADDRESS varchar(200),
-- added column
primary key (ID)
)
alter table SALES_CUSTOMER add ADDRESS varchar(200)
/db/init/postgres. Имена скриптов создания должны
иметь вид {optional_prefix}create-db.sql.
Скрипты обновления располагаются в каталоге
/db/update модуля corecore. Для каждого типа СУБД,
поддерживаемой приложением, создается свой набор
скриптов и располагается в подкаталоге с именем,
соответствующим свойству приложения cuba.dbmsType,
например, /db/update/postgres.
Скрипты обновления могут быть двух типов: с
расширением *.sql или с расширением *.groovy. SQL-
скрипты являются основным средством обновления базы
данных. Groovy-скрипты выполняются только механизмом
запуска скриптов БД сервером, поэтому применяются в
основном на этапе эксплуатации приложения - как
правило, это процессы миграции или импорта данных,
которые невозможно реализовать на SQL.
Скрипты обновления должны иметь имена, которые при
сортировке в алфавитном порядке образуют правильную
последовательность их выполнения (обычно это
хронологическая последовательность их создания).
Поэтому при ручном создании рекомендуется задавать
имя скрипта обновления в виде {yymmdd}-
{description}.sql, где yy - год, mm - месяц, dd - день,
description - краткое описание скрипта. Например,
121003-addCodeToCategoryAttribute.sql. Studio при
автоматической генерации скриптов также
придерживается этого формата.
Чтобы groovy-скрипты также выполнялись командой
updateDb, они обязательно должны иметь расширение
.upgrade.groovy и ту же логику именования, что и sql-
скрипты. Действия из набора Post update в этих скриптах
не поддерживаются, для привязки к данным
используются стандартные переменные ds (доступ к
источнику данных) и log (доступ к журналу сервера).
источнику данных) и log (доступ к журналу сервера).
Выполнение groovy-скриптов можно запретить, прописа
executeGroovy = false в команде updateDb в
build.gradle.
Скрипты обновления можно группировать в подкаталоги,
главное, чтобы путь к скрипту с учетом подкаталога не
нарушал хронологической последовательности.
Например, можно создавать подкаталоги по номеру года
или по году и месяцу.
В развернутом приложении скрипты создания и
обновления БД располагаются в специальном каталоге
скриптов базы данных, задаваемым свойством
приложения cuba.dbDir.
5.3.2.1. 5.3.2.1. Структура SQL- SQL-скриптов
SQL-скрипты создания и обновления представляют собой
текстовые файлы с набором DDL и DML команд,
разделенных символом “^”. Символ “^” применяется для
того, чтобы можно было применять разделитель “;” в
составе сложных команд, например, при создании
функций или триггеров. Механизм исполнения скриптов
разделяет входной файл на команды по разделителю “^” и
выполняет каждую команду в отдельной транзакции. Это
означает, что при необходимости можно сгруппировать
несколько простых операторов (например, insert),
разделенных точкой с запятой, и обеспечить их
выполнение в одной транзакции.
Разделитель "^" может быть заэкранирован путем его
удвоения. Например, если вам нужно передать
значение ^[0-9\s]+$, скрипт должен содержать
строку ^^[0-9\s]+$.
Пример SQL-скрипта обновления:
5.3.2.2. 5.3.2.2. Структура Groovy- Groovy-скриптов
Groovy-скрипты обновления имеют следующую структуру:
Основная
часть, содержащая код, выполняемый до
старта контекста приложения. В этой части можно
использовать любые классы Java, Groovy и блока
MiddlewareMiddleware приложения, но при этом необходимо иметь в
виду, что никакие бины, интерфейсы инфраструктуры и
прочие объекты приложения еще не инстанциированы,
и с ними работать нельзя.
Основная часть предназначена в первую очередь, как и
обычные SQL-скрипты, для обновления схемы данных.
PostUpdate
часть - набор замыканий, которые будут
выполнены после завершения процесса обновления и
create table LIBRARY_COUNTRY (
ID varchar(36) not null,
CREATE_TS time,
CREATED_BY varchar(50),
NAME varchar(100) not null,
primary key (ID)
)^
alter table LIBRARY_TOWN add column COUNTRY_ID
varchar(36) ^
alter table LIBRARY_TOWN add constraint
FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID)
references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN
(COUNTRY_ID)^
после старта контекста приложения. Внутри этих
замыканий можно оперировать любыми объектами
MiddlewareMiddleware приложения.
В этой части скрипта удобно, например, выполнять
импорт данных, так как в ней можно использовать
интерфейс Persistence и объекты модели данных.
На вход Groovy-скриптов механизм выполнения передает
следующие переменные:
ds - экземпляр javax.sql.DataSource для базы данных
приложения.
log - экземпляр org.apache.commons.logging.Log для
вывода сообщений в журнал сервера
postUpdate - объект, содержащий метод add(Closure
closure) для добавления замыканий, выполняющихся
после старта контекста сервера.
Groovy-скрипты выполняются только механизмом
запуска скриптов БД сервером.
Пример Groovy-скрипта обновления:
import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql
log.info('Executing actions in update phase')
Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION
varchar(100); """
// Add post update action
postUpdate.add({
5.3.3. 5.3.3. Выполнение скриптов БД задачами Gradle Gradle
Данный механизм применяется обычно разработчиками
приложения для собственного экземпляра базы данных.
Выполнение скриптов в этом случае сводится к запуску
специальных задач Gradle, описанных в скрипте сборки
build.gradle. Это можно сделать как из командной строки,
так и с помощью интерфейса Studio.
Для запуска скриптов
создания
БД служит задача
createDb. В Studio ей соответствует команда главного
меню RunRun → Create databaseCreate database. При запуске задачи происходит
следующее:
1. В каталоге modules/core/build/db собираются скрипты
компонентов платформы и скрипты db/**/*.sql
модуля corecore текущего проекта. Наборы скриптов
располагаются в подкаталогах с числовыми
log.info('Executing post update action using fully
functioning server')
def p = AppBeans.get(Persistence.class)
def tr = p.createTransaction()
try {
def em = p.getEntityManager()
Colour c = new Colour()
c.name = 'yellow'
c.description = 'a description'
em.persist(c)
tr.commit()
} finally {
tr.end()
}
})
располагаются в подкаталогах с числовыми
префиксами. Числовые префиксы необходимы для
соблюдения алфавитного порядка выполнения
скриптов - сначала выполняются скрипты cubacuba, затем
других компонентов, затем текущего проекта.
2. Если БД существует, она полностью очищается. Если не
существует, то создается новая пустая БД.
3. Последовательно в алфавитном порядке выполняются
все скрипты создания
modules/core/build/db/init/**/*create-db.sql, и их
имена вместе с путем относительно каталога db
регистрируются в таблице SYS_DB_CHANGELOG.
4. В таблице SYS_DB_CHANGELOG аналогично
регистрируются все имеющиеся на данный момент
скрипты обновления
modules/core/build/db/update/**/*.sql. Это
необходимо для будущего инкрементального
обновления БД новыми скриптами.
Для запуска скриптов
обновления
БД служит задача
updateDb. В Studio ей соответствует команда главного
меню RunRun → Update databaseUpdate database. При запуске задачи происходит
следующее:
1. Производится сборка скриптов аналогично описанному
выше.
2. Производится проверка, выполнены ли все скрипты
создания схемы компонентов приложения (путем
выборки из таблицы SYS_DB_CHANGELOG). Если
обнаруживается, что БД не инициализирована для
работы некоторого компонента, выполняются его
скрипты создания.
3. В каталогах modules/core/build/db/update/**
производится поиск скриптов обновления, не
зарегистрированных в таблице SYS_DB_CHANGELOG, то
есть не выполненных ранее и содержимое которых не
отражено в БД при ее инициализации.
4. Последовательно в алфавитном порядке выполняются
все найденные на предыдущем шаге скрипты, и их
имена вместе с путем относительно каталога db
регистрируются в таблице SYS_DB_CHANGELOG.
5.3.4. 5.3.4. Выполнение скриптов БД сервером
Механизм выполнения скриптов сервером предназначен
для приведения БД в актуальное состояние на старте
сервера приложения, и активируется во время
инициализации блока MiddlewareMiddleware. Понятно, что при этом
приложение должно быть собрано и развернуто на
сервере, будь то собственный Tomcat разработчика или
сервер в режиме эксплуатации.
Данный механизм в зависимости от описанных ниже
условий выполняет либо скрипты создания, либо скрипты
обновления, то есть он может и инициализировать БД с
нуля, и обновлять ее. Однако, в отличие от описанной в
предыдущем разделе задачи Gradle createDb, для
выполнения инициализации базы она должна
существовать - сервер не создает БД автоматически, а
только прогоняет на ней скрипты.
Механизм выполнения скриптов сервером действует
следующим образом:
Скрипты извлекаются из каталога скриптов базы
данных, определяемого свойством приложения
cuba.dbDir. В стандартном варианте развертывания в
Tomcat это tomcat/webapps/app-core/WEB-INF/db.
Если в БД отсутствует таблица SEC_USER, то считается,
что база данных пуста, и запускается полная
инициализация с помощью скриптов создания БД.
После выполнения инициализирующих скриптов их
После выполнения инициализирующих скриптов их
имена запоминаются в таблице SYS_DB_CHANGELOG.
Кроме того, там же сохраняются имена всех доступных
скриптов обновления,
без их выполнения
.
Если в БД имеется таблица SEC_USER, но отсутствует
таблица SYS_DB_CHANGELOG (это случай, когда в первый
раз запускается описываемый механизм на имеющейся
рабочей БД), никакие скрипты
не запускаются
. Вместо
этого создается таблица SYS_DB_CHANGELOG и в ней
сохраняются имена всех доступных на данный момент
скриптов создания и обновления.
Если в БД имеются и таблица SEC_USER и таблица
SYS_DB_CHANGELOG, то производится запуск скриптов
обновления, и их имена запоминаются в таблице
SYS_DB_CHANGELOG. Причем запускаются только те
скрипты, имен которых до этого не было в таблице
SYS_DB_CHANGELOG, т.е. не запускавшиеся ранее.
Последовательность запуска скриптов определяется
двумя факторами: приоритетом базового проекта (см.
содержимое каталога скриптов базы данных: 10-cuba,
20-workflow, …) и именем файла скрипта (с учетом
подкаталогов внутри каталога update) в алфавитном
порядке.
Перед выполнением скриптов обновления производится
проверка, все ли скрипты создания схемы компонентов
приложения выполнены (путем выборки из таблицы
SYS_DB_CHANGELOG). Если обнаруживается, что БД не
инициализирована для работы некоторого компонента,
выполняются его скрипты создания.
Механизм выполнения скриптов на старте сервера
включается свойством приложения
cuba.automaticDatabaseUpdate.
В запущенном приложении механизм выполнения
скриптов можно стартовать с помощью JMX-бина app-
скриптов можно стартовать с помощью JMX-бина app-
core.cuba:type=PersistenceManager, вызвав его метод
updateDatabase() с параметром update. Понятно, что
таким способом можно только обновить БД, а не
проинициализировать новую, так как войти в систему для
запуска метода JMX-бина при пустой БД невозможно. При
этом следует иметь в виду, что если на старте MiddlewareMiddleware
или при входе пользователя в систему начнется
инициализация той части модели данных, которая уже не
соответствует устаревшей схеме БД, то произойдет
ошибка, и продолжение работы станет невозможным.
Именно поэтому универсальным является только
автоматическое обновление БД на старте сервера перед
инициализацией модели данных.
JMX-бин app-core.cuba:type=PersistenceManager имеет
еще один метод, относящийся к механизму обновления
БД: findUpdateDatabaseScripts(). Он возвращает список
новых скриптов обновления, имеющихся в каталоге и не
зарегистрированных в БД.
Практические рекомендации по использованию
механизма обновления БД сервером приведены в
Создание и обновление БД при эксплуатации
приложения.
5.4. 5.4. Компоненты среднего слоя
На следующем рисунке приведены основные компоненты
среднего слоя CUBA-приложения.
Рисунок 13. 13. Компоненты среднего слоя
Services – управляемые контейнером компоненты,
формирующие границу приложения и предоставляющие
интерфейс клиентскому уровню приложения. Сервисы
могут содержать бизнес-логику сами, либо делегировать
выполнение Managed Beans.
Managed Beans – управляемые контейнером компоненты,
содержащие бизнес-логику приложения. Вызываются
сервисами, другими бинами или через опциональный JMX-
интерфейс.
Persistence − инфраструктурный интерфейс для доступа к
функциональности хранения данных: управлению
транзакциями и ORM.
5.4.1. 5.4.1. Сервисы
Сервисы образуют слой компонентов, определяющий
множество операций Middleware, доступных клиентскому
уровню приложения. Другими словами, сервис
представляет собой точку входа в бизнес-логику среднего
слоя. В сервисе можно управлять транзакциями,
проверять права пользователей, работать с базой данных
или делегировать выполнение другим управляемым
бинам среднего слоя.
Диаграмма классов сервиса, отображающая основные
компоненты сервиса:
компоненты сервиса:
Интерфейс сервиса располагается в модуле globalglobal и
доступен на клиентском уровне и на Middleware. Во время
выполнения, на клиентском уровне для интерфейса
сервиса создается прокси-объект, который обеспечивает
вызовы методов бина, реализующего сервис, с помощью
механизма Spring HTTP Invoker.
Бин, реализующий сервис, располагается в модуле corecore и
доступен только на среднем слое.
ServiceInterceptor автоматически вызывается для
каждого метода сервиса через Spring AOP. Он провеяет
наличие в потоке выполнения пользовательской сессии, а
также трансформирует и логирует исключения, если
сервис вызван с клиентского уровня.
5.4.1.1. 5.4.1.1. Создание сервиса
Имена интерфейсов сервисов должны заканчиваться на
Service, имена классов реализации на ServiceBean.
При создании сервиса необходимо выполнить следующее:
1. Создать интерфейс в модуле globalglobal (т.к. интерфейс
сервиса должен быть доступен на всех уровнях) и
сервиса должен быть доступен на всех уровнях) и
задать в нем имя сервиса. Имя рекомендуется задавать
в формате {имя_проекта}_{имя_интерфейса}.
Например:
2. Создать класс сервиса в модуле corecore и добавить ему
аннотацию @org.springframework.stereotype.Service
с именем, заданным в интерфейсе
Класс сервиса, как и класс любого другого управляемого
бина, должен находиться внутри дерева пакетов с
корнем, заданным в элементе context:component-scan
package com.sample.sales.core;
import com.sample.sales.entity.Order;
public interface OrderService {
String NAME = "sales_OrderService";
void calculateTotals(Order order);
}
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
@Service(OrderService.NAME)
public class OrderServiceBean implements
OrderService {
@Override
public void calculateTotals(Order order) {
}
}
корнем, заданным в элементе context:component-scan
файла spring.xml. В нашем случае файл spring.xml
содержит элемент:
что означает, что поиск аннотированных бинов для
данного блока приложения будет происходить начиная с
пакета com.sample.sales.
Если некоторую бизнес-логику требуется вызывать из
разных сервисов либо других компонентов Middleware, ее
необходимо выделить и инкапсулировать внутри
соответствующего Managed Bean. Например:
<context:component-scan base-
package="com.sample.sales"/>
// service interface
public interface SalesService {
String NAME = "sample_SalesService";
BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService
{
@Inject
private SalesCalculator salesCalculator;
@Transactional
@Override
public BigDecimal calculateSales(UUID customerId)
{
5.4.1.2. 5.4.1.2. Использование сервиса
Для того чтобы вызывать сервис, в клиентском блоке
приложения для него должен быть создан
соответствующий прокси-объект. Делается это путем
объявления имени и интерфейса сервиса в параметрах
фабрики прокси-объектов. Для блока Web Client это бин
класса WebRemoteProxyBeanCreator, для Web Portal -
PortalRemoteProxyBeanCreator, для Desktop Client -
RemoteProxyBeanCreator .
{
return
salesCalculator.calculateSales(customerId);
}
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {
@Inject
private Persistence persistence;
public BigDecimal calculateSales(UUID customerId)
{
Query query =
persistence.getEntityManager().createQuery(
"select sum(o.amount) from
sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
return (BigDecimal) query.getFirstResult();
}
}
Фабрика прокси-объектов конфигурируется в файле
spring.xml соответствующего клиентского блока.
Например, чтобы в приложении salessales вызвать с веб-
клиента сервис sales_OrderService, необходимо
добавить в файл web-spring.xml модуля webweb следующее:
Все импортируемые сервисы объявляются в одном
свойстве remoteServices в элементах map/entry.
CUBA Studio автоматически регистрирует сервисы во
всех клиентских блоках приложения.
С точки зрения прикладного кода прокси-объект сервиса
на клиентском уровне является обычным бином Spring и
может быть получен либо инжекцией, либо с помощью
класса AppBeans, например:
<bean id="sales_proxyCreator"
class="com.haulmont.cuba.web.sys.remoting.WebRemotePro
xyBeanCreator">
<property name="serverSelector"
ref="cuba_ServerSelector"/>
<property name="remoteServices">
<map>
<entry key="sales_OrderService"
value="com.sample.sales.core.OrderService"/>
</map>
</property>
</bean>
@Inject
private OrderService orderService;
public void calculateTotals() {
5.4.1.3. DataService5.4.1.3. DataService
DataService является фасадом для вызова серверной
релизации DataManager с клиентского уровня. DataService
не рекомендуется использовать в прикладном коде.
Вместо него и на клиентском уровне, и на Middleware
следует использовать DataManager.
5.4.2. Data Stores5.4.2. Data Stores
Предпочтительный способ работы с данными в CUBA-
приложениях - использование сущностей: либо
декларативно в источниках данных и связанных с
данными компонентах, либо программно через
DataManager или EntityManager. Сущности отображаются на
данные в хранилище, которое обычно представляет собой
реляционную БД. Приложение может работать с
несколькими хранилищами, так что его модель данных
будет содержать сущности, отображаемые на данные из
разных БД.
Некоторая сущность может принадлежать только одному
хранилищу. Сущности из разных хранилищ можно
отображать на одном экране UI, при этом DataManager
обеспечивает их чтение и запись в соответствующее
хранилище. В зависимости от типа сущности, DataManager
выбирает зарегистрированное хранилище,
представленное реализацией интерфейса DataStore, и
делегирует ему загрузку или сохранение. При управлении
транзакциями и работе с сущностями через
EntityManager необходимо явно указывать, какое
public void calculateTotals() {
orderService.calculateTotals(order);
}
хранилище использовать. Подробнее см. методы
интерфейса Persistence и параметры аннотации
@Transactional.
Платформа содержит единственную реализацию
интерфейса DataStore: RdbmsStore, предназначенную для
работы с реляционными СУБД через слой ORM. Кроме того,
в проекте приложения можно реализовать DataStore для
интеграции, например, с нереляционной СУБД или
внешней системой, имеющей REST интерфейс.
Каждое CUBA-приложение имеет основное хранилище,
которое содержит системные сущности и используется
для входа пользователей в приложение. В данном
руководстве под базой данных всегда имеется в виду
основное хранилище, если явно не оговорено другое.
Основное хранилище должно представлять собой
реляционную БД, подключенную через источник данных
JDBC. Источник данных основного хранилища находится в
JNDI с именем, указанным в свойстве приложения
cuba.dataSourceJndiName (по умолчанию jdbc/CubaDS).
Имена дополнительных хранилищ указываются в
свойстве приложения cuba.additionalStores. Если
дополнительное хранилище является реляционной БД
(RdbmsStore), необходимо указать для него следующие
свойства приложения:
cuba.dataSourceJndiName_{store_name} - JNDI-имя
соответствующего источника данных JDBC.
cuba.dbmsType_{store_name} - тип базы данных
хранилища.
cuba.persistenceConfig_{store_name} - путь к фалу
persistence.xml хранилища.
Если вы реализовали интерфейс DataStore в проекте,
укажите имя бина реализации в свойстве приложения
cuba.storeImpl_{store_name}.
cuba.storeImpl_{store_name}.
Предположим, что в вашем проекте два дополнительных
хранилища: db1 (база данных PostgreSQL) and mem1
(некоторое in-memory хранилище, реализованное бином
проекта). Тогда необходимо указать следующие свойства
приложения в файле app.properties модуля corecore:
Свойства cuba.additionalStores и
cuba.persistenceConfig_db1 необходимо также указать в
файлах свойств всех используемых блоков приложения
(web-app.properties, portal-app.properties, и т.д.).
CUBA Studio позволяет настраивать дополнительные
хранилища на вкладке Project properties > AdvancedProject properties > Advanced.
Studio автоматически создает все необходимые
свойства приложения и поддерживает
соответствующие файлы persistence.xml. После
этого хранилище можно будет указать в поле DataData
storestore редактора сущности. Кроме того, хранилище
можно будет выбрать при запуске мастера GenerateGenerate
modelmodel для создания сущностей, отображенных на
существующую схему БД.
Ссылки между сущностями из разных хранилищ
DataManager может автоматически поддерживать TO-ONE
ссылки между сущностями из разных хранилищ, если
они объявлены нужным образом. Например, рассмотрим
cuba.additionalStores = db1, mem1
cuba.dataSourceJndiName_db1 = jdbc/db1
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-
persistence.xml
cuba.storeImpl_mem1 = sample_InMemoryStore
случай, когда необходимо в сущности Order,
находящейся в главном хранилище, иметь ссылку на
сущность Customer из дополнительного хранилища.
Необходимо сделать следующее:
В сущности Order определить атрибут типа,
соответствующего идентификатору Customer. Атрибут
должен быть аннотирован как @SystemLevel чтобы
исключить его из различных списков, доступных
пользователям, в частности из атрибутов в Filter:
В сущности Order определить неперсистентный
атрибут-ссылку на Customer и указать атрибут
customerId как "related":
Включите неперсистентный атрибут customer в
нужные представления.
После этого, когда Order будет загружается с
представление, включающим атрибут customer,
DataManager будет автоматически загружать связанные
экземпляры Customer из дополнительного хранилища.
Загрузка коллекций оптимизирована по
производительности: после загрузки списка заказов
загрузка покупателей из доп. хранилища производится
пакетами. Размер пакета определяется свойством
приложения cuba.crossDataStoreReferenceLoadingBatchSize.
@SystemLevel
@Column(name = "CUSTOMER_ID")
private Long customerId;
@Transient
@MetaProperty(related = "customerId")
private Customer customer;
приложения cuba.crossDataStoreReferenceLoadingBatchSize.
При коммите графа объектов, включающего Order со
ссылкой на Customer, DataManager сохранит сущности
через соответствующие имплементации DataStore, а
затем сохранит идентификатор Customer в атрибуте
customerId сущности Order.
Ссылки между сущностями из разных хранилищ
поддерживаются компонентом Filter.
CUBA Studio автоматически поддерживает набор
атрибутов для ссылок между сущностями из разных
хранилищ, если в качестве ассоциации выбирается
сущность из другого хранилища.
5.4.3. 5.4.3. Интерфейс Persistence Persistence
Интерфейс Persistence является точкой входа в
функциональность хранения данных, предоставляемую
слоем ORM.
Методы интерфейса:
createTransaction(), getTransaction() - получить
интерфейс управления транзакциями. Методы могут
принимать имя хранилища данных. Если хранилище не
указано, подразумевается основная база данных.
callInTransaction(), runInTransaction() - выполнить
код в новой транзакции с возвратом значения или без
возврата значения. Методы могут принимать имя
хранилища данных. Если хранилище не указано,
подразумевается основная база данных.
isInTransaction() - определяет, существует ли в
данный момент активная транзакция.
getEntityManager() - возвращает экземпляр
EntityManager для текущей транзакции. Метод может
принимать имя хранилища данных. Если хранилище не
принимать имя хранилища данных. Если хранилище не
указано, подразумевается основная база данных.
isSoftDeletion() - позволяет определить, активен ли
режим мягкого удаления
setSoftDeletion() - устанавливает или отключает
режим мягкого удаления. Влияет на аналогичный
признак всех создаваемых экземпляров EntityManager.
По умолчанию мягкое удаление включено.
getDbTypeConverter() - возвращает экземпляр
DbTypeConverter для основной базы данных или для
дополнительного хранилища.
getDataSource() - получить javax.sql.DataSource для
основной базы данных или для дополнительного
хранилища.
Для всех объектов javax.sql.Connection,
получаемых методом
getDataSource().getConnection(), необходимо
после использования соединения вызвать метод
close() в секции finally. В противном случае
соединение не вернется в пул, через какое-то время
пул переполнится, и приложение не сможет
выполнять запросы к базе данных.
getTools() - возвращает экземпляр интерфейса
PersistenceTools (см. ниже).
5.4.3.1. PersistenceTools5.4.3.1. PersistenceTools
ManagedBean, содержащий вспомогательные методы
работы с хранилищем данных. Интерфейс
PersistenceTools можно получить либо методом
Persistence.getTools(), либо как любой другой бин -
инжекцией или через класс AppBeans.
инжекцией или через класс AppBeans.
Методы PersistenceTools:
getDirtyFields() - возвращает коллекцию имен
атрибутов сущности, измененных со времени последней
загрузки экземпляра из БД. Для новых экземпляров
возвращает пустую коллекцию.
isLoaded() - определяет, загружен ли из БД указанный
атрибут экземпляра. Атрибут может быть
не
загружен,
если он не указан в примененном при загрузке
представлении.
Данный метод работает только для экземпляров в
состоянии Managed.
getReferenceId() - возвращает идентификатор
связанной сущности без загрузки ее из БД.
Предположим, в персистентный контекст загружен
экземпляр Order, и нужно получить значение
идентификатора экземпляра Customer, связанного с
данным Заказом. Стандартное решение
order.getCustomer().getId() приведет к выполнению
SQL запроса к БД для загрузки экземпляра Customer, что
в данном случае избыточно, так как значение
идентификатора Покупателя физически находится
также и в таблице Заказов. Выполнение же
не вызовет никаких дополнительных запросов к базе
данных.
Данный метод работает только для экземпляров в
состоянии Managed.
Для расширения набора вспомогательных методов в
конкретном приложении бин PersistenceTools можно
persistence.getTools().getReferenceId(order,
"customer")
конкретном приложении бин PersistenceTools можно
переопределить. Примеры работы с расширенным
интерфейсом:
5.4.3.2. DbTypeConverter5.4.3.2. DbTypeConverter
Интерфейс, определяющий методы для конвертации
данных между значениями атрибутов модели данных и
параметрами и результатами запросов JDBC. Объект
данного интерфейса можно получить методом
Persistence.getDbTypeConverter().
Методы DbTypeConverter:
getJavaObject() - конвертирует результат JDBC запроса
в тип, подходящий для присвоения атрибуту сущности.
getSqlObject() - конвертирует значение атрибута
сущности в тип, подходящий для присвоения параметру
JDBC запроса.
getSqlType() - возвращает константу из
java.sql.Types, соответствующую переданному типу
атрибута сущности.
5.4.4. 5.4.4. Слой ORM ORM
Object-Relational Mapping - объектно-реляционное
отображение - технология связывания таблиц
реляционной базы данных с объектами языка
программирования.
MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
программирования.
Преимущества использования ORM ORM
Позволяет работать с данными реляционной СУБД,
манипулируя объектами Java.
Упрощает программирование, избавляя от рутины
написания тривиальных SQL-запросов.
Упрощает программирование, позволяя извлекать и
сохранять целые графы объектов одной командой.
Обеспечивает легкое портирование приложения на
различные СУБД.
Позволяет использовать лаконичный язык запросов
JPQL.
Недостатки
Требует понимания особенностей работы с ORM.
Усложняет оптимизацию SQL и использование
особенности применяемой СУБД.
В платформе CUBA используется реализация ORM основе
фреймворка EclipseLinkEclipseLink.
5.4.4.1. EntityManager5.4.4.1. EntityManager
EntityManager - основной интерфейс ORM, служит для
управления персистентными сущностями.
В разделе DataManager vs. EntityManager приведена
информация о различиях между EntityManager и
DataManager.
Ссылку на EntityManager можно получить через
интерфейс Persistence, вызовом метода
getEntityManager(). Полученный экземпляр
EntityManager привязан к текущей транзакции, то есть
все вызовы getEntityManager() в рамках одной
транзакции возвращают один и тот же экземпляр
транзакции возвращают один и тот же экземпляр
EntityManager. После завершения транзакции обращения
к данному экземпляру невозможны.
Экземпляр EntityManager содержит в себе
персистентный контекст
– набор экземпляров сущностей,
загруженных из БД или только что созданных.
Персистентный контекст является своего рода кэшем
данных в рамках транзакции.EntityManager
автоматически сбрасывает в БД все изменения,
сделанные в его персистентном контексте, в момент
коммита транзакции, либо при явном вызове метода
flush().
Интерфейс EntityManager, используемый в CUBA-
приложениях, в основном повторяет стандартный
javax.persistence.EntityManager. Рассмотрим его основные
методы:
persist() - вводит новый экземпляр сущности в
персистентный контекст. При коммите транзакции
командой SQL INSERT в БД будет создана
соответствующая запись.
merge() - переносит состояние отсоединенного
экземпляра сущности в персистентный контекст
следующим образом: из БД загружается экземпляр с
тем же идентификатором, в него переносится
состояние переданного Detached экземпляра и
возвращается загруженный Managed экземпляр. Далее
надо работать именно с возвращенным Managed
экземпляром. При коммите транзакции командой SQL
UPDATE в БД будет сохранено состояние данного
экземпляра.
remove() - удалить объект из базы данных, либо, если
включен режим мягкого удаления, установить атрибуты
deleteTs и deletedBy.
Если переданный экземпляр находится в Detached
состоянии, сначала выполняется merge().
find() - загружает экземпляр сущности по
идентификатору.
При формировании запроса к БД учитывается
представление, переданное в параметре данного
метода. В результате в персистентном контексте
окажется граф объектов, для которого загружены все
атрибуты представления.
В отличие от DataManager, все локальные атрибуты
сущностей загружаются наезависимо от того,
указаны ли они в представлении или нет. В
EntityManager представление влияет только на
загрузку атрибутов-ссылок.
createQuery() - создать объект Query или TypedQuery
для выполнения JPQL запроса.
createNativeQuery() - создать объект Query для
выполнения SQL запроса.
addView() - аналогичен методу setView(), но в случае
наличия уже установленного в EntityManager
представления, не заменяет его, а добавляет атрибуты
переданного представления.
reload() - перезагрузить экземпляр сущности с
указанным представлением.
isSoftDeletion() - проверяет, находится ли данный
EntityManager в режиме мягкого удаления.
setSoftDeletion() - устанавливает режим мягкого
удаления для данного экземпляра EntityManager.
getConnection() - возвращает java.sql.Connection,
через который выполняет запросы данный экземпляр
EntityManager, и, соответственно, текущая транзакция.
EntityManager, и, соответственно, текущая транзакция.
Закрывать такое соединение не нужно, оно будет
закрыто при завершении транзакции.
getDelegate() - возвращает
javax.persistence.EntityManager, предоставляемый
реализацией ORM.
Пример использования EntityManager в сервисе:
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService
{
@Inject
private Persistence persistence;
@Override
public BigDecimal calculateSales(UUID customerId)
{
BigDecimal result;
// start transaction
try (Transaction tx =
persistence.createTransaction()) {
// get EntityManager for the current
transaction
EntityManager em =
persistence.getEntityManager();
// create and execute Query
Query query = em.createQuery(
"select sum(o.amount) from
sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId",
customerId);
result = (BigDecimal)
query.getFirstResult();
// commit transaction
tx.commit();
Частичные сущности
По умолчанию, представления в EntityManager влияют
только на загрузку связей, т.е. все локальные атрибуты
всегда загружаются.
Вы можете заставить EntityManager загружать
частичные
сущности, если установите атрибут
loadPartialEntities представления в true. Однако, если
загружаемая сущность кэшируется, данный признак
игнорируется, и сущность все равно будет загружена со
всеми локальными атрибутами.
5.4.4.2. 5.4.4.2. Состояния сущности
NewNew
Только что созданный в памяти экземпляр, например:
Car car = new Car(). Новый экземпляр может быть
передан в EntityManager.persist() для сохранения в
БД, при этом он переходит в состояние Managed.
ManagedManaged
Загруженный из БД или новый, переданный в
EntityManager.persist(), экземпляр. Принадлежит
некоторому экземпляру EntityManager, другими
словами, находится в его персистентном контексте.
Любые изменения экземпляра в состоянии Managed будут
сохранены в БД в случае коммита транзакции, к которой
принадлежит данный EntityManager.
DetachedDetached
}
return result != null ? result :
BigDecimal.ZERO;
}
}
DetachedDetached
Экземпляр, загруженный из БД и отсоединенный от
своего персистентного контекста (вследствие
завершения транзакции или сериализации).
Изменения, вносимые в Detached экземпляр,
запоминаются в самом этом экземпляре (в полях,
добавленных с помощью bytecode enhancement). Эти
изменения будут сохранены в БД, только если данный
экземпляр будет снова переведен в состояние Managed
путем передачи в метод EntityManager.merge().
5.4.4.3. 5.4.4.3. Загрузка по требованию
Загрузка по требованию (lazy loading) позволяет загружать
связанные сущности отложенно, т.е. только в момент
первого обращения к их свойствам.
Загрузка по требованию в сумме порождает больше
запросов к БД, чем жадная загрузка (eager fetching), однако
нагрузка при этом растянута во времени.
Например, при извлечении списка N экземпляров
сущности A, содержащих ссылку на экземпляр сущности
B, в случае загрузки по требованию будет выполнено
N+1 запросов к базе данных.
Как правило, для минимизации времени отклика и
снижения нагрузки необходимо стремиться к меньшему
количеству обращений к БД. Для этого в платформе
используется механизм представлений, с помощью
которого в вышеописанном случае ORM может
сформировать один запрос к БД с объединением
таблиц.
Загрузка по требованию работает только для экземпляра
в состоянии Managed, то есть внутри транзакции,
загрузившей данный экземпляр.
5.4.4.4. 5.4.4.4. Выполнение JPQL JPQL запросов
Для выполнения JPQL запросов предназначен интерфейс
Query, ссылку на который можно получить у текущего
экземпляра EntityManager вызовом метода
createQuery(). Если запрос предполагается использовать
для извлечения сущностей, рекомендуется вызывать
createQuery() с передачей типа результата, что приведет
к созданию TypedQuery.
Методы Query в основном соответствуют методам
стандартного интерфейса JPA javax.persistence.Query.
Рассмотрим отличия.
setParameter() - устанавливает значение параметра
запроса. При передаче в данный метод экземпляра
сущности выполняет неявное преобразование
экземпляра в его идентификатор. Например:
Обратите внимание на сравнение в запросе по
идентификатору, но передачу в качестве параметра
самого экземпляра сущности.
Вариант метода с передачей implicitConversions =
false не выполняет такого преобразования.
setView(), addView() - устанавливают представление,
используемое при загрузке данных.
getDelegate() - возвращает экземпляр
Customer customer = ...;
TypedQuery<Order> query = entityManager.createQuery(
"select o from sales$Order o where o.customer.id
= ?1", Order.class);
query.setParameter(1, customer);
javax.persistence.Query, предоставляемый
реализацией ORM.
Если для Query установлено представление, то по
умолчанию Query имеет FlushModeType.AUTO, что влияет
на случай, когда в текущем персистентном контексте
содержатся измененные экземпляры сущностей: эти
экземпляры будут сохранены в БД перед выполнением
запроса. Другими словами, ORM сначала синхронизирует
состояние сущностей в персистентном контексте и в БД, а
уже потом выполняет запрос. Этим гарантируется, что в
результаты запроса попадут все соответствующие
экземпляры, даже если они еще не были сохранены в базе
данных явно. Обратной стороной этого является неявный
ush, т.е. выполнение команд SQL update для всех
измененных в данном контексте сущностей, что может
повлиять на производительность.
Если же Query выполняется без представления, то по
умолчанию Query имеет FlushModeType.COMMIT, что
означает, что неявный ush вызван не будет, и запрос не
будет учитывать содержимое текущего персистентного
контекста.
В большинстве случаев игнорирование текущего
персистентного контекста допустимо, и является
предпочтительным поведением, так как не вызывает
дополнительных команд SQL. Однако, при использовании
представлений существует следующая проблема: если в
персистентном контексте есть измененный экземпляр
сущности, и выполняется запрос с представлением и
FlushModeType.COMMIT, загружающий этот же экземпляр,
то изменения будут потеряны. Поэтому по умолчанию мы
используем FlushModeType.AUTO для запросов с
представлением.
Вы также можете явно установить ush mode с помощью
метода setFlushMode() интерфейса Query, чтобы
переопределить режим по умолчанию.
5.4.4.4.1. 5.4.4.4.1. Функции JPQL JPQL
В таблице ниже описаны функции JPQL, поддерживаемые
и не поддерживаемые платформой CUBA.
Функция Поддерживается Пример запроса
Агрегатные
функции
ДА SELECT AVG(o.quantity) FROM app$Order
o
НЕТ: агрегатные
функции со
скалярными
выражениями
(особенность
EclipseLink)
SELECT AVG(o.quantity)/2.0 FROM
app$Order o
SELECT AVG(o.quantity * o.price) FROM
app$Order o
ALL, ANY, SOME ДА SELECT emp FROM app$Employee emp
WHERE emp.salary > ALL (SELECT
m.salary FROM app$Manager m WHERE
m.department = emp.department)
Арифметические
функции (INDEX,
SIZE, ABS, SQRT, MOD)
ДА SELECT w.name FROM app$Course c JOIN
c.studentWaitlist w WHERE c.name =
'Calculus' AND INDEX(w) = 0
SELECT w.name FROM app$Course c WHERE
c.name = 'Calculus' AND
SIZE(c.studentWaitlist) = 1
SELECT w.name FROM app$Course c WHERE
c.name = 'Calculus' AND ABS(c.time) =
10
SELECT w.name FROM app$Course c WHERE
c.name = 'Calculus' AND SQRT(c.time)
= 10.5
SELECT w.name FROM app$Course c WHERE
c.name = 'Calculus' AND MOD(c.time,
c.name = 'Calculus' AND MOD(c.time,
c.time1) = 2
CASE ДА SELECT e.name, f.name, CONCAT(CASE
WHEN f.annualMiles > 50000 THEN
'Platinum ' WHEN f.annualMiles >
25000 THEN 'Gold ' ELSE '' END,
'Frequent Flyer') FROM app$Employee e
JOIN e.frequentFlierPlan f
НЕТ: CASE в
UPDATE-запросе UPDATE app$Employee e SET e.salary =
CASE e.rating WHEN 1 THEN e.salary *
1.1 WHEN 2 THEN e.salary * 1.05 ELSE
e.salary * 1.01 END
Функции даты и
времени
(CURRENT_DATE,
CURRENT_TIME,
CURRENT_TIMESTAMP)
ДА SELECT e FROM app$Order e WHERE
e.date = CURRENT_DATE
Функции EclipseLink
(CAST, REGEXP,
EXTRACT)
ДА SELECT EXTRACT(YEAR FROM e.createTs)
FROM app$MyEntity e WHERE
EXTRACT(YEAR FROM e.createTs) > 2012
SELECT e FROM app$MyEntity e WHERE
e.name REGEXP '.*'
SELECT CAST(e.number text) FROM
app$MyEntity e WHERE e.path LIKE
CAST(:ds$myEntityDs.id text)
НЕТ: CAST в
запросе GROUP
BY
SELECT e FROM app$Order e WHERE
e.amount > 100 GROUP BY
CAST(e.orderDate date)
Операторы типов
сущности
ДА: тип
сущности
передаётся как
параметр
SELECT e FROM app$Employee e WHERE
TYPE(e) IN (:empType1, :empType2)
НЕТ: прямая
ссылка на
сущность
SELECT e FROM app$Employee e WHERE
TYPE(e) IN (app$Exempt,
Функция Поддерживается Пример запроса
сущность TYPE(e) IN (app$Exempt,
app$Contractor)
Вызов функций ДА: результат с
операторами
сравнения
SELECT u FROM sec$User u WHERE
function('DAYOFMONTH', u.createTs) =
1
НЕТ: прямое
использование
результата
функции
SELECT u FROM sec$User u WHERE
function('hasRoles', u.createdBy,
u.login)
IN ДА SELECT e FROM Employee e,
IN(e.projects) p WHERE p.budget >
1000000
IS EMPTY для
коллекций
ДА SELECT e FROM Employee e WHERE
e.projects IS EMPTY
KEY/VALUE
НЕТ SELECT v.location.street,
KEY(i).title, VALUE(i) FROM
app$VideoStore v JOIN
v.videoInventory i WHERE
v.location.zipcode = '94301' AND
VALUE(i) > 0
Литералы ДА SELECT e FROM app$Employee e WHERE
e.name = 'Bob'
SELECT e FROM app$Employee e WHERE
e.id = 1234
SELECT e FROM app$Employee e WHERE
e.id = 1234L
SELECT s FROM app$Stat s WHERE
s.ratio > 3.14F
SELECT s FROM app$Stat s WHERE
s.ratio > 3.14e32D
SELECT e FROM app$Employee e WHERE
e.active = TRUE
Функция Поддерживается Пример запроса
e.active = TRUE
НЕТ: литералы
даты и времени SELECT e FROM app$Employee e WHERE
e.startDate = {d'2012-01-03'}
SELECT e FROM app$Employee e WHERE
e.startTime = {t'09:00:00'}
SELECT e FROM app$Employee e WHERE
e.version = {ts'2012-01-03
09:00:00.000000001'}
MEMBER OF ДА: для полей и
запросов SELECT d FROM app$Department d WHERE
(select e from app$Employee e where
e.id = :eParam) MEMBER OF e.employees
НЕТ: для
литералов SELECT e FROM app$Employee e WHERE
'write code' MEMBER OF e.codes
NEW в SELECT
НЕТ SELECT NEW
com.acme.example.CustomerDetails(c.id,
c.status, o.count) FROM app$Customer
c JOIN c.orders o WHERE o.count > 100
NULLIF/COALESCE ДА SELECT NULLIF(emp.salary, 10) FROM
app$Employee emp
SELECT COALESCE(emp.salary,
emp.salaryOld, 10) FROM app$Employee
emp
NULLS FIRST, NULLS
LAST в order by
ДА SELECT h FROM sec$GroupHierarchy h
ORDER BY h.level DESC NULLS FIRST
Строковые функции
(CONCAT, SUBSTRING,
TRIM, LOWER, UPPER,
LENGTH, LOCATE)
ДА SELECT x FROM app$Magazine x WHERE
CONCAT(x.title, 's') = 'JDJs'
SELECT x FROM app$Magazine x WHERE
SUBSTRING(x.title, 1, 1) = 'J'
SELECT x FROM app$Magazine x WHERE
LOWER(x.title) = 'd'
Функция Поддерживается Пример запроса
SELECT x FROM app$Magazine x WHERE
UPPER(x.title) = 'D'
SELECT x FROM app$Magazine x WHERE
LENGTH(x.title) = 10
SELECT x FROM app$Magazine x WHERE
LOCATE('A', x.title, 4) = 6
SELECT x FROM app$Magazine x WHERE
TRIM(TRAILING FROM x.title) = 'D'
НЕТ: TRIM не
поддерживается
с trim char
SELECT x FROM app$Magazine x WHERE
TRIM(TRAILING 'J' FROM x.title) = 'D'
Вложенные
запросы
ДА SELECT goodCustomer FROM app$Customer
goodCustomer WHERE
goodCustomer.balanceOwed < (SELECT
AVG(c.balanceOwed) FROM app$Customer
c)
НЕТ: path-
выражения
вместо имени
сущности в FROM
подзапроса
SELECT c FROM app$Customer c WHERE
(SELECT AVG(o.price) FROM c.orders o)
> 100
TREAT ДА SELECT e FROM app$Employee e JOIN
TREAT(e.projects AS app$LargeProject)
p WHERE p.budget > 1000000
НЕТ: TREAT в
WHERE-
выражениях
SELECT e FROM Employee e JOIN
e.projects p WHERE TREAT(p as
LargeProject).budget > 1000000
Функция Поддерживается Пример запроса
5.4.4.4.2. 5.4.4.4.2. Поиск подстроки без учета регистра
Для удобного формирования условия поиска без учета
регистра символов и по любой части строки можно
использовать префикс (?i) в значении параметра
запроса. Например, имеется запрос:
запроса. Например, имеется запрос:
Если в значении параметра name передать строку (?
i)%doe%, то при наличии в БД записи со значением John
Doe она будет найдена, несмотря на раличие в регистре
символа. Это произойдет потому, что ORM выполнит SQL с
условием вида lower(C.NAME) like ?.
Следует иметь в виду, что при таком поиске индекс,
созданный в БД по полю NAME, не используется.
5.4.4.4.3. 5.4.4.4.3. Макросы в JPQL JPQL
Текст JPQL запроса может включать макросы, которые
обрабатываются перед выполнением и превращаются в
исполняемый JPQL, дополнительно модифицируя набор
параметров.
Макросы решают следующие задачи:
Позволяют обойти принципиальную невозможность
средствами JPQL выразить условие зависимости
значения поля от текущего момента времени (не
работает арифметика типа current_date-1)
Позволяют сравнивать с датой поля типа Timestamp
(содержащие дату+время)
Рассмотрим их подробно:
@between@between
Имеет вид @between(field_name, moment1, moment2,
time_unit), где
field_name - имя атрибута для сравнения
moment1, moment2 - моменты времени, в которые должно
попасть значение атрибута field_name. Каждый из
моментов должен быть определен выражением с
select c from sales$Customer c where c.name like :name
участием переменной now, к которой может быть
прибавлено или отнято целое число
time_unit - определяет единицу измерения времени,
которое прибавляется или вычитается из now в
выражениях моментов, а также точность округления
моментов. Может быть следующим: year, month, day,
hour, minute, second.
Макрос преобразуется в следующее выражение JPQL:
field_name >= :moment1 and field_name < :moment2
Пример 1. Покупатель создан сегодня:
Пример 2. Покупатель создан в течение последних 10
минут:
Пример 3. Документы, датированные последними 5
рабочими днями (для проектов, включающих workowworkow):
@today@today
Имеет вид @today(field_name) и обеспечивает
формирование условия попадания значения атрибута в
текущий день. По сути это частный случай макроса
@between.
Пример. Пользователь создан сегодня:
select c from sales$Customer where
@between(c.createTs, now, now+1, day)
select c from sales$Customer where
@between(c.createTs, now-10, now, minute)
select d from sales$Doc where @between(d.createTs,
now-5, now, workday)
Пример. Пользователь создан сегодня:
@dateEquals@dateEquals
Имеет вид @dateEquals(field_name, parameter) и
позволяет сформировать условие попадания значения
поля field_name типа Timestamp в дату, задаваемую
параметром parameter.
Пример:
@dateBefore@dateBefore
Имеет вид @dateBefore(field_name, parameter) и
позволяет сформировать условие, что дата значения
поля field_name типа Timestamp меньше даты,
задаваемой параметром parameter.
Пример:
@dateAfter@dateAfter
Имеет вид @dateAfter(field_name, parameter) и
позволяет сформировать условие, что дата значения
поля field_name типа Timestamp больше или равна дате,
задаваемой параметром parameter.
Пример:
select d from sales$Doc where @today(d.createTs)
select d from sales$Doc where
@dateEquals(d.createTs, :param)
select d from sales$Doc where
@dateBefore(d.createTs, :param)
select d from sales$Doc where @dateAfter(d.createTs,
@enum@enum
Позволяет использовать полное имя константы enum
вместо ее идентификатора в БД. Это упрощает поиск
использований enum в коде приложения.
Пример:
5.4.4.5. 5.4.4.5. Выполнение SQL SQL запросов
ORM позволяет выполнять SQL запросы к базе данных,
возвращая как списки отдельных полей, так и экземпляры
сущностей. Для этого необходимо создать объект Query
или TypedQuery вызовом одного из методов
EntityManager.createNativeQuery().
Если выполняется выборка отдельных колонок таблицы,
то результирующий список будет содержать строки в
виде Object[]. Например:
select d from sales$Doc where @dateAfter(d.createTs,
:param)
select r from sec$Role where r.type =
@enum(com.haulmont.cuba.security.entity.RoleType.SUPE
R) order by r.name
Query query =
persistence.getEntityManager().createNativeQuery(
"select ID, NAME from SALES_CUSTOMER where
NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
Object[] row = (Object[]) it.next();
UUID id = (UUID) row[0];
Если выполняется выборка единственной колонки или
агрегатной функции, то результирующий список будет
содержать эти значения непосредственно:
Если вместе с текстом запроса передан класс
результирующей сущности, то возвращается TypedQuery и
после выполнения производится попытка отображения
результатов запроса на атрибуты сущности. Например:
Следует иметь в виду, что при использовании SQL
колонки, соответствующие атрибутам сущностей типа
UUID, возвращаются в виде UUID или в виде String, в
зависимости от используемой СУБД:
HSQLDBHSQLDB - String
PostgreSQLPostgreSQL - UUID
Microsoft SQL ServerMicrosoft SQL Server - String
OracleOracle - String
UUID id = (UUID) row[0];
String name = (String) row[1];
}
Query query =
persistence.getEntityManager().createNativeQuery(
"select count(*) from SEC_USER where login =
#login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();
TypedQuery<Customer> query = em.createNativeQuery(
"select * from SALES_CUSTOMER where NAME like ?1",
Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();
OracleOracle - String
MySQLMySQL – String
Параметры этого типа также должны задаваться либо как
UUID, либо своим строковым представлением, в
зависимости от используемой СУБД. Для обеспечения
независимости кода от используемой СУБД
рекомендуется использовать DbTypeConverter, который
обеспечивает конвертацию данных между объектами Java
и параметрами и результатами JDBC.
В SQL запросах можно использовать позиционные или
именованные параметры. Позиционные параметры
обозначаются ? с последующим номером параметра
начиная с 1. Именованные параметры обозначаются
знаком #. См. примеры выше.
Поведение SQL запросов, возвращающих сущности, и
модифицирующих запросов (update, delete), по
отношению к текущему персистентному контексту
аналогично описанному для JPQL запросов.
См. также Выполнение SQL с помощью QueryRunner.
5.4.4.6. Entity Listeners5.4.4.6. Entity Listeners
Entity Listeners
предназначены для реакции на события
жизненного цикла экземпляров сущностей на уровне
Middleware.
Слушатель представляет собой класс, реализующий один
или несколько интерфейсов пакета
com.haulmont.cuba.core.listener. Слушатель будет
реагировать на события типов, соответствующих
реализуемым интерфейсам.
BeforeDetachEntityListenerBeforeDetachEntityListener
Метод onBeforeDetach() вызывается перед отделением
Метод onBeforeDetach() вызывается перед отделением
объекта от EntityManager при коммите транзакции.
Данный слушатель можно использовать, например, для
заполнения неперсистентных атрибутов сущности
перед отправкой ее на клиентский уровень.
BeforeAttachEntityListenerBeforeAttachEntityListener
Метод onBeforeAttach() вызывается перед введением
объекта в персистентный контекст при выполнении
операции EntityManager.merge().
Данный слушатель можно использовать, например, для
заполнения персистентных атрибутов сущности перед
сохранением ее в базе данных.
BeforeInsertEntityListenerBeforeInsertEntityListener
Метод onBeforeInsert() вызывается перед
выполнением вставки записи в БД. В данном методе
возможны любые операции с текущим EntityManager.
AfterInsertEntityListenerAfterInsertEntityListener
Метод onAfterInsert() вызывается после выполнения
вставки записи в БД, но до коммита транзакции. В
данном методе нельзя модифицировать текущий
персистентный контекст, однако можно производить
изменения в БД с помощью QueryRunner.
BeforeUpdateEntityListenerBeforeUpdateEntityListener
Метод onBeforeUpdate() вызывается перед изменением
записи в БД. В данном методе возможны любые
операции с текущим EntityManager.
AfterUpdateEntityListenerAfterUpdateEntityListener
Метод onAfterUpdate() вызывается после изменения
записи в БД, но до коммита транзакции. В данном методе
нельзя модифицировать текущий персистентный
контекст, однако можно производить изменения в БД с
помощью QueryRunner.
BeforeDeleteEntityListenerBeforeDeleteEntityListener
BeforeDeleteEntityListenerBeforeDeleteEntityListener
Метод onBeforeDelete() вызывается перед удалением
записи из БД (или в случае мягкого удаления - перед
изменением записи). В данном методе возможны любые
операции с текущим EntityManager.
AfterDeleteEntityListenerAfterDeleteEntityListener
Метод onAfterDelete() вызывается после удаления
записи из БД (или в случае мягкого удаления - после
изменения записи), но до коммита транзакции. В данном
методе нельзя модифицировать текущий
персистентный контекст, однако можно производить
изменения в БД с помощью QueryRunner.
Entity Listener должен являться управляемым бином,
поэтому в нем можно использовать инжекцию:
@Component("sample_MyEntityListener")
public class MyEntityListener implements
BeforeInsertEntityListener<MyEntity>,
BeforeUpdateEntityListener<MyEntity> {
@Inject
protected Metadata metadata;
@Override
public void onBeforeInsert(MyEntity entity,
EntityManager entityManager) {
Foo foo = metadata.create(Foo.class);
...
entity.setFoo(foo);
entityManager.persist(foo);
}
@Override
public void onBeforeUpdate(MyEntity entity,
EntityManager entityManager) {
Foo foo = entityManager.find(Foo.class,
Entity Listener может быть задан двумя способами:
Статически - имена бинов слушателей указываются в
аннотации @Listeners на классе сущности:
Динамически - имя бина слушателя передается в метод
addListener() бина EntityListenerManager. Пример
динамического добавления слушателя рассматривается
в разделе рецептов разработки: Выполнение кода на
старте приложения.
Для всех экземпляров некоторого класса сущности
создается
один
экземпляр слушателя определенного
типа, поэтому слушатель
не должен иметь состояния
.
Если для сущности объявлены несколько слушателей
одного типа (например, аннотациями класса сущности и
его предков, плюс динамически), то их вызов будет
выполняться в следующем порядке:
1. Для каждого предка, начиная с самого дальнего,
вызываются его динамически добавленные слушатели,
затем статически назначенные.
2. После всех предков вызываются динамически
Foo foo = entityManager.find(Foo.class,
entity.getFoo().getId());
...
}
}
@Entity(...)
@Table(...)
@Listeners("sample_MyEntityListener")
public class MyEntity extends StandardEntity {
...
}
2. После всех предков вызываются динамически
добавленные слушатели данного класса, затем
статически назначенные.
Если требуются некоторые вычисления и изменение
атрибутов сразу
нескольких
экземпляров сущностей
при коммите транзакции, рассмотрите также
возможность использования transaction listener.
5.4.5. 5.4.5. Управление транзакциями
В данном разделе рассмотрены различные аспекты
управления транзакциями в CUBA-приложениях.
5.4.5.1. 5.4.5.1. Программное управление
транзакциями
Программное управление транзакциями осуществляется
с помощью интерфейса
com.haulmont.cuba.core.Transaction, ссылку на который
можно получить методами createTransaction() или
getTransaction() интерфейса инфраструктуры
Persistence.
Метод createTransaction() создает новую транзакцию и
возвращает интерфейс Transaction. Последующие
вызовы методов commit(), commitRetaining(), end() этого
интерфейса управляют созданной транзакцией. Если в
момент создания существовала другая транзакция, то она
будет приостановлена, и возобновлена после завершения
созданной.
Метод getTransaction() вызывает либо создание новой,
либо присоединение к текущей транзакции. Если в
момент вызова существовала активная транзакция, то
метод успешно завершается, и последующие вызовы
метод успешно завершается, и последующие вызовы
commit(), commitRetaining(), end() не оказывают
никакого влияния на существующую транзакцию. Однако
если end() вызван без предварительного вызова
commit(), то текущая транзакция помечается как
RollbackOnly.
Примеры программного управления транзакцией:
Интерфейс Transaction имеет также метод execute(),
принимающий на вход класс-действие или lambda-
@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction())
{
Customer customer =
metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
Customer customer =
metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
} finally {
tx.end();
}
принимающий на вход класс-действие или lambda-
выражение, которое нужно выполнить в данной
транзакции. Это позволяет организовать управление
транзакциями в функциональном стиле, например:
Следует иметь в виду, что метод execute() у некоторого
экземпляра Transaction можно вызвать только один раз,
так как после выполнения кода действия транзакция
завершается.
5.4.5.2. 5.4.5.2. Декларативное управление
транзакциями
Любой метод управляемого бина Middleware можно
пометить аннотацией
@org.springframework.transaction.annotation.Transac
tional, что вызовет автоматическое создание транзакции
при вызове этого метода. В таком методе не нужно
вызывать Persistence.createTransaction(), а можно
UUID customerId =
persistence.createTransaction().execute((EntityManager
em) -> {
Customer customer =
metadata.create(Customer.class);
customer.setName("ABC");
em.persist(customer);
return customer.getId();
});
Customer customer =
persistence.createTransaction().execute(em ->
em.find(Customer.class, customerId,
"_local"));
вызывать Persistence.createTransaction(), а можно
сразу получать EntityManager и работать с ним.
Для аннотации @Transactional можно указать
параметры, в том числе:
propagation - режим создания транзакции. Значение
REQUIRED соответствует getTransaction(), значение
REQUIRES_NEW - createTransaction(). По умолчанию
REQUIRED.
value - имя хранилища данных. Если опущено,
подразумевается основная БД. Например:
Декларативное управление транзакциями позволяет
уменьшить количество boilerplate кода, однако имеет
следующий недостаток: коммит транзакции происходит
вне прикладного кода, что часто затрудняет отладку, т.к.
скрывается момент отправки изменений в БД и перехода
сущностей в состояние Detached. Кроме того, следует
иметь в виду, что декларативная разметка сработает
только в случае вызова метода контейнером, т.е. вызов
транзакционного метода из другого метода того же
самого объекта не приведет к старту транзакции.
В связи с этим рекомендуется применять декларативное
управление транзакциями только для простых случаев,
@Transactional(propagation =
Propagation.REQUIRES_NEW)
public void doSomething() {
}
@Transactional("db1")
public void doSomething() {
}
таких как методы сервисов, читающих некоторый объект
и возвращающих его клиенту.
5.4.5.3. 5.4.5.3. Примеры взаимодействия
транзакций
5.4.5.3.1. 5.4.5.3.1. Откат вложенной транзакции
Если вложенная транзакция создана через
getTransaction(), то ее откат приведет к невозможности
коммита охватывающей транзакции. Например:
void methodA() {
Transaction tx = persistence.createTransaction();
try {
// (1) calling a method creating a nested
transaction
methodB();
// (4) at this point an exception will be
thrown, because transaction
// is marked as rollback only
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.getTransaction();
try {
// (2) let us assume the exception occurs here
tx.commit();
} catch (Exception e) {
// (3) handle it and exit
Если же транзакция в methodB() будет создана через
createTransaction(), то ее откат не окажет никакого
влияния на коммит охватывающей транзакции в
methodA().
5.4.5.3.2. 5.4.5.3.2. Чтение и изменение данных во вложенной транзакции
Рассмотрим сначала зависимую вложенную транзакцию,
создаваемую через getTransaction():
// (3) handle it and exit
return;
} finally {
tx.end();
}
}
void methodA() {
Transaction tx = persistence.createTransaction();
try {
EntityManager em =
persistence.getEntityManager();
// (1) loading an entity with name == "old
name"
Employee employee = em.find(Employee.class,
id);
assertEquals("old name", employee.getName());
// (2) setting new value to the field
employee.setName("name A");
// (3) calling a method creating a nested
transaction
methodB();
// (8) the changes are committed to DB, and
Теперь рассмотрим тот же самый пример с независимой
вложенной транзакцией, создаваемой через
// it will contain "name B"
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.getTransaction();
try {
// (4) retrieving the same instance of
EntityManager as methodA
EntityManager em =
persistence.getEntityManager();
// (5) loading an entity with the same
identifier
Employee employee = em.find(Employee.class,
id);
// (6) the field value is the new one since we
are working with the same
// persistent context, and there are no calls
to DB at all
assertEquals("name A", employee.getName());
employee.setName("name B");
// (7) no actual commit is done at this point
tx.commit();
} finally {
tx.end();
}
}
createTransaction():
void methodA() {
Transaction tx = persistence.createTransaction();
try {
EntityManager em =
persistence.getEntityManager();
// (1) loading an entity with name == "old
name"
Employee employee = em.find(Employee.class,
id);
assertEquals("old name", employee.getName());
// (2) setting new value to the field
employee.setName("name A");
// (3) calling a method creating a nested
transaction
methodB();
// (8) an exception occurs due to optimistic
locking
// and commit will fail
tx.commit();
} finally {
tx.end();
}
}
void methodB() {
Transaction tx = persistence.createTransaction();
try {
// (4) creating a new instance of
EntityManager,
// as this is a new transaction
В последнем случае исключение в точке (8) возникнет,
только если сущность является оптимистично
блокируемой, т.е. если она реализует интерфейс
Versioned.
5.4.5.4. 5.4.5.4. Параметры транзакций
Таймаут транзакции
Для создаваемой транзакции может быть указан
таймаут в секундах, при превышении которого
транзакция будет прервана и откачена. Таймаут
EntityManager em =
persistence.getEntityManager();
// (5) loading an entity with the same
identifier
Employee employee = em.find(Employee.class,
id);
// (6) the field value is old because an old
instance of the entity
// has been loaded from DB
assertEquals("old name", employee.getName());
employee.setName("name B");
// (7) the changes are commited to DB, and the
value of
// "name B" will now be in DB
tx.commit();
} finally {
tx.end();
}
}
транзакция будет прервана и откачена. Таймаут
транзакции ограничивает максимальную длительность
запросов к базе данных.
При программном управлении транзакциями таймаут
включается путем передачи объекта TransactionParams
в метод Persistence.createTransaction(). Например:
При декларативном управлении транзакциями
используется параметр timeout аннотации
@Transactional, например:
Таймаут по умолчанию может быть задан в свойстве
приложения cuba.defaultQueryTimeoutSec.
Read-only Read-only транзации
Транзакцию можно пометить как read-only если она
предназначена только для чтения данных из БД.
Например, все методы load в DataManager используют
read-only транзакции по умолчанию. Read-only транзакции
улучшают производительность системы, потому что
платформа не выполняет код, обрабатывающий
возможные изменения в сущностях. Кроме того, не
вызываются BeforeCommit transaction listeners.
Если персистентный контекст read-only транзакции
содержит измененные сущности, то при попытке
коммита транзакции будет выброшено исключение
IllegalStateException. Это означает, что помечать
Transaction tx = persistence.createTransaction(new
TransactionParams().setTimeout(2));
@Transactional(timeout = 2)
public void someServiceMethod() {
...
IllegalStateException. Это означает, что помечать
транзакцию как read-only следует только если вы
уверены, что она не модифицирует никакие
сущности.
При программном управлении транзакциями признак
read-only включается путем передачи объекта
TransactionParams в метод
Persistence.createTransaction(). Например:
При декларативном управлении транзакциями
используется параметр readOnly аннотации
@Transactional, например:
5.4.5.5. Transaction Listeners5.4.5.5. Transaction Listeners
Transaction listeners предназначены для реакции на события
жизненного цикла транзакций. В отличие от entity listeners,
они не привязаны к типу сущности и вызываются для
каждой транзакции.
Слушатель должен быть управляемым бином,
реализующим один или оба интерфейса
BeforeCommitTransactionListener и
AfterCompleteTransactionListener.
BeforeCommitTransactionListenerBeforeCommitTransactionListener
Transaction tx = persistence.createTransaction(new
TransactionParams().setReadOnly(true));
@Transactional(readOnly = true)
public void someServiceMethod() {
...
Метод beforeCommit() вызывается перед коммитом
транзакции после всех entity listeners если транзакция не
является read-only. Метод принимает текущий
EntityManager и коллекцию сущностей текущего
персистентного контекста.
Данный слушатель можно использовать для
обеспечения сложных бизнес-правил, вовлекающих
различные сущности. В примере ниже атрибут amount
сущности Order должен рассчитываться на основе
значения discount, находящегося в заказе, и атрибутов
price и quantity экземпляров сущности OrderLine,
составляющих заказ.
@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements
BeforeCommitTransactionListener {
@Inject
private PersistenceTools persistenceTools;
@Override
public void beforeCommit(EntityManager
entityManager, Collection<Entity> managedEntities) {
// gather all orders affected by changes in
the current transaction
Set<Order> affectedOrders = new HashSet<>();
for (Entity entity : managedEntities) {
// skip not modified entities
if (!persistenceTools.isDirty(entity))
continue;
if (entity instanceof Order)
affectedOrders.add((Order) entity);
else if (entity instanceof OrderLine) {
AfterCompleteTransactionListenerAfterCompleteTransactionListener
Метод afterComplete() вызывается после завершения
транзакции. Метод принимает параметр, указывающий,
была ли транзакция успешно закоммичена, и коллекцию
detached сущностей, содержавшихся в персистентном
контексте завершенной транзакции.
Order order = ((OrderLine)
entity).getOrder();
// a reference can be detached, so
merge it into current persistence context
affectedOrders.add(entityManager.merge(order));
}
}
// calculate amount for each affected order
by its lines and discount
for (Order order : affectedOrders) {
BigDecimal amount = BigDecimal.ZERO;
for (OrderLine orderLine :
order.getOrderLines()) {
if (!orderLine.isDeleted()) {
amount =
amount.add(orderLine.getPrice().multiply(orderLine.ge
tQuantity()));
}
}
BigDecimal discount =
order.getDiscount().divide(BigDecimal.valueOf(100),
2, BigDecimal.ROUND_DOWN);
order.setAmount(amount.subtract(amount.multiply(disco
unt)));
}
}
}
контексте завершенной транзакции.
Пример использования:
5.4.6. 5.4.6. Кэши сущностей и запросов
Кэш сущностей (Entity Cache) (Entity Cache)
Кэш сущностей предоставляется ORM фреймворком
EclipseLink. Он хранит в памяти недавно прочитанные или
записанные экземпляры сущностей, тем самым
сокращая доступ к базе данных и увеличивая
производительность.
Кэш сущностей используется только при извлечении
сущностей по идентификатору, поэтому запросы по
@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements
AfterCompleteTransactionListener {
private Logger log =
LoggerFactory.getLogger(OrdersTransactionListener.cla
ss);
@Override
public void afterComplete(boolean committed,
Collection<Entity> detachedEntities) {
if (!committed)
return;
for (Entity entity : detachedEntities) {
if (entity instanceof Order) {
log.info("Order: " + entity);
}
}
}
}
сущностей по идентификатору, поэтому запросы по
другим атрибутам по прежнему выполняются на базе
данных. Тем не менее, эти запросы могут стать проще и
быстрее, если связанные сущности находятся в кэше.
Например, если вы запрашиваете Заказы вместе со
связанными Заказчиками, и не используете кэш, то SQL-
запрос будет содержать JOIN с таблицей заказчиков.
Если же сущность Заказчик закэширована, SQL-запрос
будет только по таблице заказов, а связанные заказчики
будут извлечены из кэша.
Для того, чтобы включить кэш сущностей, установите
следующие свойства приложения в файле app.properties
модуля corecore вашего проекта:
eclipselink.cache.shared.sales$Customer = true -
включает кэширование сущности sales$Customer.
eclipselink.cache.size.sales$Customer = 500 -
устанавливает размер кэша для сущности
sales$Customer в 500 экземпляров. Размер по
умолчанию - 100.
Факт кэширования сущности влияет на то, какой fetch
mode выбирается платформой при загрузке графов
сущностей. Если некоторый ссылочный атрибут
представляет собой кэшируемую сущность, то fetch mode
всегда будет UNDEFINED, что позволяет ORM извлекать
ссылку из кэша вместо добавления в запрос JOIN или
выполнения отдельного batch-запроса.
Платформа обеспечивает координацию кэша сущностей
в кластере middleware. Когда кэшированный экземпляр
сущности обновляется или удаляется на одном узле
кластера, тот же экземпляр на других узлах (если он
загружен) будет инвалидирован, что приведет к
загрузке свежего состояния из БД при следующей
операции с данным экземпляром.
Кэш запросов (Query Cache) (Query Cache)
Кэш запросов (Query Cache) (Query Cache)
Кэш запросов сохраняет идентификаторы экземпляров
сущностей, возвращаемых JPQL-запросами, тем самым
естественно дополняя кэш сущностей.
Например, если для сущности sales$Customer разрешен
entity cache, и запрос select c from sales$Customer c
where c.grade = :grade выполняется первый раз,
происходит следующее:
ORM выполняет запрос в базе данных.
Загруженные экземпляры сущности Customer
помещаются в entity cache.
В кэш запросов помещается соответствие между
текстом запроса вместе с параметрами и списком
идентификаторов загруженных экземпляров.
Когда этот же запрос с такими же параметрами
выполняется второй раз, платформа находит запрос в
кэше запросов и загружает экземпляры сущностей по
идентификаторам из кэша сущностей, не обращаясь к
базе данных.
Запросы по умолчанию не кэшируются. Указать, что
запрос должен кэшироваться, можно на различных
уровнях приложения:
Методом setCacheable() интерфейса Query при работе
с EntityManager.
Методом setCacheable() интерфейса
LoadContext.Query при работе с DataManager.
Методом setCacheable() интерфейса
CollectionDatasource или в XML-атрибуте cacheable
при работе с источниками данных.
Кэшируемые запросы следует использовать только
если для возвращаемой сущности разрешен entity
cache. В противном случае при каждом запросе
cache. В противном случае при каждом запросе
экземпляры сущности будут загружаться из базы
данных по идентификаторам по одному.
Кэш запросов автоматически инвалидируется, когда
через ORM выполняются операции создания, изменения
или удаления с сущностями соответствующего типа.
Инвалидация работает по всему кластеру среднего слоя.
JMX-бин app-core.cuba:type=QueryCacheSupport можно
использовать для мониторинга состояния кэша и для
удаления запросов из кэша. Например, если вы изменили
некоторый экземпляр сущности sales$Customer
напрямую в БД, необходимо удалить все
закэшированные запросы по этой сущности с помощью
операции evict() с аргументом sales$Customer.
На поведение кэша запросов оказывают влияние
следующие свойства приложения:
cuba.queryCache.enabled
cuba.queryCache.maxSize
5.4.7. 5.4.7. Системная аутентификация
При выполнении пользовательских запросов
программному коду Middleware через интерфейс
UserSessionSource всегда доступна информация о текущем
пользователе. Это возможно потому, что при получении
запроса с клиентского уровня в потоке выполнения
автоматически устанавливается соответствующий
объект SecurityContext.
Однако существуют ситуации, когда текущий поток
выполнения не связан ни с каким пользователем системы:
например, при вызове метода бина из планировщика, либо
через JMX-интерфейс. Если при этом бин выполняет
изменение сущностей в базе данных, то ему потребуется
информация о том, кто выполняет изменения, то есть
аутентификация.
Такого рода аутентификация называется системной, так
как не требует участия пользователя - средний слой
приложения просто создает (или использует имеющуюся)
пользовательскую сессию, и устанавливает в потоке
выполнения соответствующий объект SecurityContext.
Обеспечить системную аутентификацию некоторого
участка кода можно следующими способами:
явно используя бин
com.haulmont.cuba.security.app.Authentication,
например:
добавив методу бина аннотацию @Authenticated,
например:
Во втором случае также используется бин
Authentication, но неявно, через интерцептор
@Inject
protected Authentication authentication;
...
authentication.begin();
try {
// authenticated code
} finally {
authentication.end();
}
@Authenticated
public String foo(String value) {
// authenticated code
}
Authentication, но неявно, через интерцептор
AuthenticationInterceptor, который перехватывает
вызовы всех методов бинов с аннотацией @Authenticated.
В приведенных примерах пользовательская сессия будет
создаваться от лица пользователя, логин которого указан
в свойстве приложения cuba.jmxUserLogin. Если требуется
аутентификация от имени другого пользователя, нужно
воспользоваться первым вариантом и передать в метод
begin() логин нужного пользователя.
Если в момент выполнения Authentication.begin() в
текущем потоке выполнения присутствует активная
пользовательская сессия, то она не заменяется -
соответственно, код, требующий аутентификации,
будет выполняться с имеющейся сессией, и
последующий метод end() не будет очищать поток.
Например, вызов метода JMX-бина из встроенной в
Web ClientWeb Client консоли JMX, если бин находится в той же
JVM, что и блок WebClient, к которому в данный момент
подключен пользователь, будет выполнен от имени
текущего зарегистрированного в системе
пользователя, независимо от наличия системной
аутентификации.
5.5. 5.5. Универсальный пользовательский
интерфейс
Подсистема универсального пользовательского
интерфейса (Generic UI, GUI) позволяет разрабатывать
экраны пользовательского интерфейса, используя XML и
Java. Созданные таким образом экраны одинаково
работоспособны в двух стандартных клиентских блоках:
Web ClientWeb Client и Desktop ClientDesktop Client.
Рисунок 14. 14. Структура универсального пользовательского интерфейса
Здесь в центре изображены основные составляющие
экранов универсального пользовательского интерфейса:
XML-дескрипторы - файлы XML, содержащие
информацию об источниках данных и компоновке
экрана
Контроллеры - классы Java, содержащие логику
инициализации экрана и обработки событий от
элементов пользовательского интерфейса.
Код экранов приложения, расположенный в модуле guigui,
взаимодействует с интерфейсами визуальных
компонентов (VCL Interfaces), реализованными по-
отдельности в модулях webweb и desktopdesktop базового проекта
cubacuba. Для Web ClientWeb Client реализация основана на фреймворке
VaadinVaadin, для Desktop ClientDesktop Client – на фреймворке Java SwingJava Swing.
Библиотека визуальных компонентов (Visual Components
Library, VCL) содержит большой набор готовых
компонентов для отображения данных.
Механизм источников данных (Datasources) предоставляет
унифицированный интерфейс, обеспечивающий
функционирование связанных с данными визуальных
компонентов.
Инфраструктура клиента (Infrastructure) включает в себя
главное окно приложения, механизмы отображения и
взаимодействия экранов UI, а также средства
взаимодействия со средним слоем.
5.5.1. 5.5.1. Экраны
Экран универсального пользовательского интерфейса
состоит из XML-дескриптора и класса контроллера.
Дескриптор содержит ссылку на класс контроллера.
Для того чтобы экран можно было вызывать из главного
меню или из Java кода (например, из контроллера другого
экрана), XML-дескриптор должен быть зарегистрирован в
файле screens.xml проекта.
Главное меню приложения формируется отдельно для
Web ClientWeb Client и Desktop ClientDesktop Client на основе файлов menu.xml,
расположенных соответственно в модулях webweb и desktopdesktop
проекта.
5.5.1.1. 5.5.1.1. Типы экранов
В данном разделе рассматриваются основные типы
экранов:
Фрейм
Простой экран
Экран выбора
Экран редактирования
Комбинированный экран
5.5.1.1.1. 5.5.1.1.1. Фрейм
Фреймы представляют собой части экранов, которые
применяются для декомпозиции и многократного
использования. Для подключения фрейма в XML экрана
используется элемент frame.
Контроллер фрейма должен быть унаследован от класса
AbstractFrame.
Фрейм можно создать в Studio с помощью шаблона
Blank frameBlank frame.
Правила взаимодействия экрана и вложенного в него
фрейма:
Из экрана обращаться к компонентам фрейма можно
через точку: frame_id.component_id
Из контроллера фрейма получить компонент экрана
можно обычным вызовом getComponent(component_id),
но только в том случае, если компонент с таким именем
не объявлен в самом фрейме. То есть компоненты
фрейма маскируют компоненты экрана.
Из фрейма получить источник данных экрана можно
простым вызовом getDsContext().get(ds_id) или
инжекцией, либо в запросе ds$ds_id, но только в том
случае, если источник данных с таким именем не
объявлен в самом фрейме (аналогично компонентам).
Из экрана получить источник данных фрейма можно
только через итерацию по
getDsContext().getChildren()
При коммите экрана вызывается также коммит
измененных источников данных всех вложенных
фреймов.
5.5.1.1.2. 5.5.1.1.2. Простой экран
Простой экран предназначен для отображения и
редактирования произвольной информации, в том числе
отдельных экземпляров и списков сущностей. Данный тип
экрана имеет только базовую функциональность,
позволяющую отобразить его в главном окне системы и
работать с источниками данных.
Контроллер простого экрана должен быть унаследован от
класса AbstractWindow.
Простой экран можно создать в Studio с помощью
шаблона Blank screenBlank screen.
5.5.1.1.3. 5.5.1.1.3. Экран выбора
Экран выбора (lookup) предназначен для выбора и
возврата экземпляров и списков сущностей. Стандартное
действие LookupAction в таких визуальных компонентах,
как PickerField и LookupPickerField, вызывает экраны выбора
для поиска связанных сущностей.
При вызове экрана выбора методом openLookup()
отображается панель с кнопками для выбора. Когда
пользователь выбирает один или несколько экземпляров,
экран вызывает переданный ему обработчик, тем самым
возвращая вызывающему коду результаты выбора. При
вызове методом openWindow() или, например, из главного
меню, панель с кнопками выбора не отображается, что
превращает экран выбора в простой экран.
Контроллер экрана выбора должен быть унаследован от
класса AbstractLookup. В XML экрана в атрибуте
lookupComponent должен быть указан компонент
(например, Table), из которого будет взят экземпляр
сущности при выборе.
Экран выбора для сущности можно создать в Studio с
помощью шаблонов Entity browserEntity browser или Entity combinedEntity combined
screenscreen.
По умолчанию, действие LookupAction использует экран,
зарегистрованный в файле screens.xml с идентификатором
вида {имя
сущности}.lookup или
{имясущности}.browse,
например, sales$Customer.lookup. Поэтому при
использовании вышеупомянутых компонентов убедитесь,
что такой экран создан. Studio регистрирует browse-экраны
с идентификаторами вида {имя_сущности}.browse,
поэтому они автоматически вызываются в качестве
экранов выбора.
Настройка вида и поведения экрана выбора
Для того, чтобы заменить панель выбора (кнопки SelectSelect
и CancelCancel) для всех экранов выбора в проекте, создайте
фрейм и зарегистрируйте его с идентификатором
lookupWindowActions. Стандартный фрейм
расположен в файле /com/haulmont/cuba/gui/lookup-
window.actions.xml. Новый фрейм должен содержать
кнопку, связанную с действием lookupSelectAction
(которое автоматически добавляется в экран, когда он
открывается как экран выбора).
Чтобы заменить панель выбора в некотором экране,
создайте в нем кнопку, связанную с действием
lookupSelectAction. В этом случае стандартный
фрейм не будет показан. Например:
<layout expand="table">
<hbox>
<button id="selectBtn" caption="Select
item"
action="lookupSelectAction"/>
Чтобы заменить стандартное действие выбора своим,
просто добавьте его в контроллере:
В качестве базового класса используйте
com.haulmont.cuba.gui.components.SelectAction,
переопределив требуемые методы.
5.5.1.1.4. 5.5.1.1.4. Экран редактирования
Экран редактирования предназначен для отображения и
редактирования экземпляра сущности. Поддерживает
функциональность установки редактируемого
экземпляра и действия по коммиту изменений в базу
данных. Экран редактирования должен вызываться
методом openEditor() с передачей экземпляра сущности.
Стандартные действия CreateAction и EditAction открывают
</hbox>
<!-- ... -->
</layout>
@Override
public void init(Map<String, Object> params) {
addAction(new SelectAction(this) {
@Override
protected Collection
getSelectedItems(LookupComponent lookupComponent)
{
Set<MyEntity> selected = new HashSet<>
();
// ...
return selected;
}
});
}
экран, зарегистрированный в файле screens.xml с
идентификатором вида {имя_сущности}.edit, например,
sales$Customer.edit.
Контроллер экрана редактирования должен быть
унаследован от класса AbstractEditor.
Экран редактирования для сущности можно создать
в Studio с помощью шаблона Entity editorEntity editor.
В XML экрана в атрибуте datasource указывается
источник данных, в который проставляется
редактируемый экземпляр сущности. Для отображения
действий, выполняющих коммит или отмену изменений, в
XML можно использовать следующие стандартные
фреймы с кнопками:
editWindowActions (файл
com/haulmont/cuba/gui/edit-window.actions.xml) -
содержит кнопки OKOK и CancelCancel
extendedEditWindowActions (файл
com/haulmont/cuba/gui/extended-edit-
window.actions.xml) - содержит кнопки OK & CloseOK & Close, OKOK и
CancelCancel
В экране редактирования неявно создаются следующие
действия:
windowCommitAndClose (соответствует константе
Window.Editor.WINDOW_COMMIT_AND_CLOSE) - действие,
выполняющее коммит изменений в базу данных и
закрывающее экран. Создается при наличии в экране
визуального компонента с идентификатором
windowCommitAndClose, в частности, при использовании
вышеописанного стандартного фрейма
extendedEditWindowActions отображается кнопкой OK &OK &
CloseClose.
windowCommit (соответствует константе
Window.Editor.WINDOW_COMMIT) - действие,
выполняющее коммит изменений в базу данных. При
отсутствии действия windowCommitAndClose после
коммита закрывает экран. Создается всегда, и при
наличии в экране вышеописанных стандартных
фреймов отображается кнопкой OKOK.
windowClose (соответствует константе
Window.Editor.WINDOW_CLOSE) - действие, закрывающее
экран без коммита изменений. Создается всегда, и при
наличии в экране вышеописанных стандартных
фреймов отображается кнопкой CancelCancel.
Таким образом, если в экран добавлен фрейм
editWindowActions, то кнопка OKOK коммитит изменения и
закрывает экран, а кнопка CancelCancel - закрывает без коммита.
Если же добавлен фрейм extendedEditWindowActions, то
кнопка OKOK только коммитит изменения, оставляя экран
открытым, кнопка OK & CloseOK & Close коммитит и закрывает экран,
кнопка CancelCancel - закрывает без коммита.
Вместо стандартных фреймов для отображения действий
можно использовать произвольные компоненты,
например, LinkButton.
5.5.1.1.5. 5.5.1.1.5. Комбинированный экран
Комбинированный экран позволяет отобразить список
сущностей слева и форму редактирования выбранного
экземпляра справа. Таким образом, это комбинация
экранов выбора и редактирования.
Контроллер экрана редактирования должен быть
унаследован от класса EntityCombinedScreen.
Комбинированный экран для сущности можно
создать в Studio с помощью шаблона Entity combinedEntity combined
screenscreen.
screenscreen.
5.5.1.2. XML-5.5.1.2. XML-дескриптор
XML-дескриптор - это файл формата XML, описывающий
источники данных и расположение визуальных
компонентов экрана.
Схема XML доступна по адресу
http://schemas.haulmont.com/cuba/6.7/window.xsd.
Рассмотрим структуру дескриптора.
window − корневой элемент.
Атрибуты window:
class − имя класса контроллера
messagesPack − пакет сообщений данного экрана,
который будет использован при получении
локализованных строк без указания пакета из XML-
дескриптора и из контроллера методом getMessage()
caption − заголовок экрана, может содержать ссылку на
сообщение из вышеуказанного пакета, например,
focusComponent − идентификатор компонента, который
получит фокус ввода при отображении экрана
lookupComponent - обязательный для экрана выбора
атрибут, задающий идентификатор визуального
компонента, из которого будет выбран экземпляр
сущности. Поддерживаются компоненты следующих
типов (и их наследников):
Table
Tree
caption="msg://caption"
Tree
LookupField
PickerField
OptionsGroup
datasource - обязательный для экрана редактирования
атрибут, задающий идентификатор источника данных, в
который будет проставлен экземпляр редактируемой
сущности.
Элементы window:
metadataContext − элемент для инициализации
представлений (views), необходимых данному экрану.
Предпочтительным является определение всех
представлений в одном общем файле views.xml, так как
все описатели представлений разворачиваются в один
общий репозиторий, и при рассредоточении описателей
по разным файлам трудно обеспечить уникальность
имен.
dsContext − определяет источники данных данного
экрана.
dialogMode - определяет параметры геометрии и
поведения экрана при открытии его в виде диалогового
окна.
Атрибуты dialogMode:
closeable - определяет наличие в диалоговом окне
кнопки закрытия. Возможные значения: true, false.
closeOnClickOutside - определяет возможность
закрыть окно кликом по окружающей области, если
диалог открыт в модальном режиме. Возможные
значения: true, false.
forceDialog - указывает, что экран должен всегда
открываться в режиме диалога, независимо от того,
какой WindowManager.OpenType был выбран в
вызывающем коде. Возможные значения: true, false.
height - устанавливает высоту диалогового окна.
maximized - если выбрано значение true, диалог будет
развёрнут во весь экран. Возможные значения: true,
false.
modal - устанавливает модальный режим дилогового
окна. Возможные значения: true, false.
positionX - задаёт положение левого верхнего угла
диалога по оси x.
positionY - задаёт положение левого верхнего угла
диалога по оси y.
resizable - определяет возможность пользователя
изменять размеры диалога. Возможные значения: true,
false.
width - устанавливает ширину диалогового окна.
Пример использования dialogMode:
actions - определяет список действий данного экрана.
timers - определяет список таймеров данного экрана.
companions - определяет список классов-компаньонов
данного контроллера
Элементы companions:
web - задает компаньон, реализованный в модуле webweb
desktop - задает компаньон, реализованный в модуле
<dialogMode height="600"
width="800"
positionX="200"
positionY="200"
forceDialog="true"
closeOnClickOutside="false"
resizable="true"/>
desktop - задает компаньон, реализованный в модуле
desktopdesktop
Каждый из этих элементов содержит атрибут class,
задающий класс компаньона.
layout − корневой элемент компоновки экрана.
Является сам по себе контейнером с вертикальным
расположением компонентов, аналогичным vbox.
Атрибуты layout:
spacing
margin
expand
stylename
height
width
5.5.1.3. 5.5.1.3. Контроллер экрана
Контроллер экрана - это Java или Groovy класс, связанный
с XML-дескриптором, и содержащий логику
инициализации и обработки событий экрана.
Контроллер должен быть унаследован от одного из
следующих базовых классов:
AbstractFrame − предназначен для реализации фреймов.
AbstractWindow − предназначен для реализации простых
экранов.
AbstractLookup − предназначен для реализации экранов
выбора.
AbstractEditor − предназначен для реализации экранов
редактирования.
Если экрану не нужна никакая дополнительная
логика, то в качестве контроллера можно
использовать сам базовый класс AbstractWindow,
AbstractLookup или AbstractEditor, указав его в
XML-дескрипторе (эти классы на самом деле не
являются абстрактными в смысле невозможности
создания экземпляров). Для фрейма класс
контроллера можно не указывать вообще.
Класс контроллера должен быть зарегистрирован в XML-
дескрипторе экрана в атрибуте class корневого элемента
window.
Рисунок 15. 15. Базовые классы контроллеров
5.5.1.3.1. AbstractFrame5.5.1.3.1. AbstractFrame
AbstractFrame является корнем иерархии классов
контроллеров. Рассмотрим его основные методы:
init() - вызывается фреймворком после создания всего
дерева компонентов, описанного XML-дескриптором, но
до отображения экрана.
В метод init() из вызывающего кода передается мэп
параметров, которые могут быть использованы внутри
контроллера. Эти параметры могут быть переданы как
из кода контроллера вызывающего экрана (в методе
openWindow(), openLookup() или openEditor()), так и
установлены в файле регистрации экранов screens.xml.
установлены в файле регистрации экранов screens.xml.
Метод init() следует имплементировать при
необходимости инициализации компонентов экрана,
например:
getMessage(), formatMessage() - методы получения
локализованных сообщений из пакета, заданного для
экрана в XML-дескрипторе. Представляют собой просто
короткие варианты вызова одноименных методов
интерфейса Messages.
openFrame() - загрузить фрейм по идентификатору,
зарегистрированному в screens.xml, и, если в метод
передан компонент-контейнер, отобразить его внутри
контейнера. Возвращается контроллер фрейма.
Например:
@Inject
private Table someTable;
@Override
public void init(Map<String, Object> params) {
someTable.addGeneratedColumn("someColumn", new
Table.ColumnGenerator<Colour>() {
@Override
public Component generateCell(Colour entity)
{
...
}
});
}
@Inject
private BoxLayout container;
Контейнер не обязательно сразу передавать в метод
openFrame(), вместо этого можно загрузить фрейм, а
затем добавить его в нужный контейнер:
openWindow(), openLookup(), openEditor() - открыть
соответственно простой экран, экран выбора или
редактирования. Методы возвращают контроллер
созданного экрана.
При открытии экрана в режиме диалога метод
openWindow() может быть вызван с параметрами, к
примеру:
@Override
public void init(Map<String, Object> params) {
SomeFrame frame = openFrame(container,
"someFrame");
frame.setHeight("100%");
frame.someInitMethod();
}
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
SomeFrame frame = openFrame(null, "someFrame");
frame.setHeight("100%");
frame.someInitMethod();
container.add(frame);
}
@Override
public void actionPerform(Component component) {
openWindow("sec$User.browse",
Эти параметры будут учитываться, если они не
конфликтуют с более приоритетными параметрами
самого вызываемого экрана. Последние могут быть
заданы в методе getDialogOptions() контроллера экрана
или в XML-дескрипторе этого экрана:
Для выполнения действий после закрытия вызываемого
экрана необходимо добавить слушатель типа
CloseListener, например:
CloseWithCommitListener можно использовать в случае,
если необходимо реагировать только при закрытии
экрана действием с именем Window.COMMIT_ACTION_ID
(то есть кнопкой OKOK), например:
WindowManager.OpenType.DIALOG.width(800).height(300)
.closeable(true).resizable(true).modal(false));
}
<dialogMode forceDialog="true" width="300"
height="200" closeable="true" modal="true"
closeOnClickOutside="true"/>
CustomerEdit editor =
openEditor("sales$Customer.edit", customer,
WindowManager.OpenType.THIS_TAB);
editor.addCloseListener((String actionId) -> {
// do something
});
CustomerEdit editor =
openEditor("sales$Customer.edit", customer,
WindowManager.OpenType.THIS_TAB);
showMessageDialog() - отобразить диалоговое окно с
сообщением.
showOptionDialog() - отобразить диалоговое окно с
сообщением и возможностью выбора пользователем
некоторых действий. Действия задаются массивом
объектов типа Action, которые в диалоге отображаются
посредством соответствующих кнопок.
Для отображения стандартных кнопок типа OKOK, CancelCancel и
других рекомендуется использовать объекты типа
DialogAction, например:
showNotification() - отобразить всплывающее окно с
сообщением.
showWebPage() - открыть указанную веб-страницу в
браузере.
editor.addCloseWithCommitListener(() -> {
// do something
});
showOptionDialog("PLease confirm", "Are you sure?",
MessageType.CONFIRMATION,
new Action[] {
new DialogAction(DialogAction.Type.YES)
{
@Override
public void actionPerform(Component
component) {
// do something
}
},
new DialogAction(DialogAction.Type.NO)
});
APIAPI
formatMessage() - getMessage() - init() - openFrame() -
openEditor() - openLookup() - openWindow() -
showMessageDialog() - showNotication() - showOptionDialog() -
showWebPage()
5.5.1.3.2. AbstractWindow5.5.1.3.2. AbstractWindow
AbstractWindow является наследником AbstractFrame, и
определяет следующие собственные методы:
getDialogOptions() - получить объект DialogOptions
для управления геометрией и поведением экрана когда
он открывается в режиме диалога
(WindowManager.OpenType.DIALOG). Эти параметры могут
быть заданы при инициализации экрана, а также могут
изменяться на лету.
Установка ширины и высоты:
Установка положения диалога на экране:
Возможность закрыть диалог кликом по окружающей
области:
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setWidth("480px").setHeight("320p
x");
}
getDialogOptions()
.setPositionX(100)
.setPositionY(100);
области:
Указание того, что диалог должен быть немодальным и
с изменяемыми размерами:
Указание того, что диалог должен быть развёрнут во
весь экран:
Указание того, что экран должен всегда открываться в
режиме диалога, независимо от того, какой
WindowManager.OpenType был выбран в вызывающем
коде:
setContentSwitchMode() - определяет режим
переключения вкладок главного TabSheet для вкладки,
содержащей данное окно: скрывать содержимое или
getDialogOptions().setModal(true).setCloseOnClickOut
side(true);
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setModal(false).setResizable(true
);
}
getDialogOptions().setMaximized(true);
@Override
public void init(Map<String, Object> params) {
getDialogOptions().setForceDialog(true);
}
содержащей данное окно: скрывать содержимое или
полностью выгружать его.
Доступны следующие режимы:
DEFAULT - режим переключения определяется режимом
главного TabSheet, установленном в свойстве
приложения cuba.web.managedMainTabSheetMode.
HIDE - содержимое вкладки должно быть только скрыто,
независимо от режима главного TabSheet.
UNLOAD - содержимое вкладки должно быть выгружено,
независимо от режима главного TabSheet.
saveSettings() - сохраняет настройки экрана для
текущего пользователя в базе данных при закрытии
экрана.
К примеру, на экране имеется чекбокс showPanelshowPanel,
управляющий отображением некой панели. Мы
переопределяем метод saveSettings(): создаём в нём
XML-элемент для этого чекбокса, добавляем ему атрибут
showPanel, содержащий текущее значение чекбокса, а
затем сохраняем элемент settings в XML-файл для
текущего пользователя в базе данных.
@Inject
private CheckBox showPanel;
@Override
public void saveSettings() {
boolean showPanelValue = showPanel.getValue();
Element xmlDescriptor =
getSettings().get(showPanel.getId());
xmlDescriptor.addAttribute("showPanel",
String.valueOf(showPanelValue));
super.saveSettings();
}
applySettings() - восстанавливает настройки экрана
для текущего пользователя из базы данных при
открытии экрана.
Переопределим метод для восстановления настроек из
предыдущего примера. Получаем XML-элемент
чекбокса, проверяем, что нужный нам атрибут
showPanel не равен null, а затем восстанавливаем для
чекбокса предыдущее сохранённое значение:
Другой пример управления настройками экрана можно
увидеть на стандартном экране Server LogServer Log в меню
AdministrationAdministration приложения CUBA, который автоматически
сохраняет и восстанавливает последние открытые
пользователем лог-файлы.
ready() - шаблонный метод, который можно
имплементировать в контроллере для перехвата
момента открытия экрана. Метод ready() вызывается
фреймворком после метода init() непосредственно
перед показом экрана в главном окне приложения.
validateAll() - валидация экрана. Реализация по
умолчанию вызывает метод validate() у всех
@Override
public void applySettings(Settings settings) {
super.applySettings(settings);
Element xmlDescriptor =
settings.get(showPanel.getId());
if (xmlDescriptor.attribute("showPanel") !=
null) {
showPanel.setValue(Boolean.parseBoolean(xmlDescripto
r.attributeValue("showPanel")));
}
}
умолчанию вызывает метод validate() у всех
компонентов экрана, реализующих интерфейс
Component.Validatable, накапливает информацию об
исключениях, и если таковые имеются, выводит
соответствующее сообщение и возвращает false, иначе
возвращает true.
Данный метод следует переопределять только в том
случае, если необходимо полностью заменить
стандартную процедуру валидации экрана. Если же
нужно только дополнить ее, достаточно определить
специальный шаблонный метод postValidate().
postValidate() - шаблонный метод, который можно
имплементировать в контроллере для дополнительной
валидации экрана. Получаемый методом объект
ValidationErrors используется для добавления
информации об ошибках валидации, которая будет
отображена совместно с ошибками стандартной
валидации. Например:
close() - закрыть данный экран.
private Pattern pattern = Pattern.compile("\\d");
@Override
protected void postValidate(ValidationErrors errors)
{
if (getItem().getAddress().getCity() != null) {
if
(pattern.matcher(getItem().getAddress().getCity()).f
ind()) {
errors.add("City name can't contain
digits");
}
}
}
close() - закрыть данный экран.
Метод принимает строковое значение, передаваемое
далее в шаблонный метод preClose() и слушателям
CloseListener. Таким образом, заинтересованный код
может получить информацию о причине закрытия
экрана от кода, инициирующего закрытие. В частности,
в экранах редактирования сущностей при закрытии
экрана после коммита изменений рекомендуется
использовать константу Window.COMMIT_ACTION_ID, без
коммита изменений - константу
Window.CLOSE_ACTION_ID.
Если какой-либо из источников данных содержит
несохраненные изменения, перед закрытием экрана
будет выдано диалоговое окно с соответствующим
предупреждением. Тип предупреждения можно
выбрать с помощью свойства приложения
cuba.gui.useSaveConfirmation.
Вариант метода close() с параметром force = true
закрывает экран без вызова preClose() и без
предупреждения, независимо от наличия
несохраненных изменений.
Метод close() возвращает true, если экран был
успешно закрыт, и false - если закрытие было прервано.
preClose() - шаблонный метод, который можно
имплементировать в контроллере для перехвата
момента закрытия экрана. Метод получает строковое
значение, указанное инициатором закрытия при вызове
метода close().
Если метод preClose() возвращает false, то процесс
закрытия экрана прерывается.
addBeforeCloseWithCloseButtonListener() - добавляет
слушатель, который отслеживает закрытие окна одним
из следующих способов: кнопка закрытия окна, панель
breadcrumbs или действия для закрытия вкладок
breadcrumbs или действия для закрытия вкладок
TabSheet (Close, Close All, Close Others). Чтобы
предотвратить случайное закрытие окна
пользователем, можно вызвать метод
preventWindowClose() события BeforeCloseEvent:
addBeforeCloseWithShortcutListener - добавляет
слушатель, который отслеживает закрытие окна
горячими клавишами (например, нажатием Esc). Чтобы
предотвратить случайное закрытие окна
пользователем, можно вызвать метод
preventWindowClose() события BeforeCloseEvent:
APIAPI
addBeforeCloseWithCloseButtonListener() -
addBeforeCloseWithShortcutListener() - applySettings() - close() -
setContentSwitchMode() - getDialogOptions() - postValidate() -
preClose() - ready() - saveSettings() - validateAll()
5.5.1.3.3. AbstractLookup5.5.1.3.3. AbstractLookup
AbstractLookup базовый класс контроллеров экранов
выбора, является наследником AbstractWindow, и
определяет следующие собственные методы:
setLookupComponent() - установить компонент, из
которого будет производиться выбор экземпляров
сущности.
addBeforeCloseWithCloseButtonListener(BeforeCloseEve
nt::preventWindowClose);
addBeforeCloseWithShortcutListener(BeforeCloseEvent:
:preventWindowClose);
сущности.
Как правило, компонент выбора устанавливается в XML-
дескрипторе экрана, и вызывать данный метод в
прикладном коде нет необходимости.
setLookupValidator() - установить для экрана объект
типа Window.Lookup.Validator, метод validate()
которого вызывается фреймворком перед тем как
вернуть выбранные экземпляры сущностей. Если
validate() возвращает false, процесс выбора и
закрытия экрана прерывается.
По умолчанию валидатор не установлен.
APIAPI
setLookupComponent() - setLookupValidator()
5.5.1.3.4. AbstractEditor5.5.1.3.4. AbstractEditor
AbstractEditor − базовый класс контроллеров экранов
редактирования, является наследником AbstractWindow.
При создании конкретного класса контроллера
рекомендуется параметризовать AbstractEditor типом
редактируемой сущности. При этом методы getItem() и
initNewItem() будут работать с конкретным типом
сущности и прикладному коду не потребуется
дополнительных приведений типов. Например:
AbstractEditor определяет следующие собственные
public class CustomerEdit extends
AbstractEditor<Customer> {
@Override
protected void initNewItem(Customer item) {
...
AbstractEditor определяет следующие собственные
методы:
getItem() - возвращает экземпляр редактируемой
сущности, установленный в главном источнике данных
экрана (т.е. указанном в атрибуте datasource корневого
элемента XML-дескриптора).
Если редактируется не новый экземпляр, то в момент
открытия экрана он перезагружается из базы данных с
необходимым представлением, указанным для главного
источника данных.
Изменения, вносимые в экземпляр, возвращаемый
getItem(), отражаются на состоянии источника данных,
и будут отправлены на MiddlewareMiddleware при коммите экрана.
Следует иметь в виду, что getItem() возвращает
значение только после инициализации экрана
методом setItem(). До этого момента, например, в
методах init() и initNewItem(), данный метод
возвращает null.
Однако в методе init() экземпляр сущности,
переданный в openEditor(), можно получить из
параметров следующим образом:
В метод initNewItem() экземпляр передается явно
и нужного типа.
В обоих случаях полученный экземпляр сущности,
если он не новый, будет впоследствии
@Override
public void init(Map<String, Object> params) {
Customer item =
WindowParams.ITEM.getEntity(params);
// do something
}
если он не новый, будет впоследствии
перезагружен, и вносить в него изменения или
сохранять в поле для последующего использования
не имеет смысла.
setItem() - вызывается фреймворком при открытии
экрана методом openEditor() для установки
редактируемого экземпляра сущности в главном
источнике данных. В момент вызова созданы все
компоненты и источники данных экрана, и отработал
метод init() контроллера.
Для инициализации экрана редактирования вместо
переопределения setItem() рекомендуется
имплементировать специальные шаблонные методы
initNewItem() и postInit().
initNewItem() - шаблонный метод, вызываемый
фреймворком перед установкой редактируемого
экземпляра сущности в главном источнике данных.
Метод initNewItem() вызывается только для
нового, только что созданного экземпляра
сущности. Если редактируется detached экземпляр,
метод не вызывается.
Данный метод можно имплементировать в контроллере
при необходимости инициализации нового экземпляра
сущности перед его установкой в источник данных,
например:
@Inject
private UserSession userSession;
@Override
protected void initNewItem(Complaint item) {
item.setOpenedBy(userSession.getUser());
item.setStatus(ComplaintStatus.OPENED);
Более сложный пример использования initNewItem()
приведен в разделе рецептов разработки.
postInit() - шаблонный метод, вызываемый
фреймворком сразу после установки редактируемого
экземпляра сущности в главном источнике данных. Во
время выполнения данного метода можно вызывать
getItem(), который будет возвращать новый или
перезагруженный при инициализации экрана
экземпляр сущности.
Данный метод можно имплементировать в контроллере
для окончательной инициализации экрана, например:
commit() - валидировать экран и отправить изменения
через DataSupplier на MiddlewareMiddleware.
Если используется вариант метода с параметром
validate = false, то валидация перед коммитом не
производится.
Данный метод не рекомендуется переопределять,
лучше использовать специальные шаблонные методы
postValidate(), preCommit() и postCommit().
item.setStatus(ComplaintStatus.OPENED);
}
@Inject
protected EntityDiffViewer diffFrame;
@Override
protected void postInit() {
if (!PersistenceHelper.isNew(getItem())) {
diffFrame.loadVersions(getItem());
}
}
commitAndClose() - валидировать экран, отправить
изменения на MiddlewareMiddleware и закрыть экран. В метод
preClose() и зарегистрированным слушателям
CloseListener будет передано значение константы
Window.COMMIT_ACTION_ID.
Данный метод не рекомендуется переопределять,
лучше использовать специальные шаблонные методы
postValidate(), preCommit() и postCommit().
preCommit() - шаблонный метод, вызываемый
фреймворком в процессе коммита изменений, после
того как валидация завершена успешно и перед
отправкой данных на MiddlewareMiddleware.
Данный метод можно имплементировать в
контроллере. Если метод возвращает false, процесс
коммита (и закрытия экрана, если был вызван
commitAndClose()), прерывается. Например:
postCommit() - шаблонный метод, вызываемый
фреймворком на финальной стадии коммита
изменений. Параметры метода:
committed - установлен в true, если в экране
действительно были изменения, и они отправлены на
MiddlewareMiddleware;
@Override
protected boolean preCommit() {
if (somethingWentWrong) {
showNotification("Something went wrong",
NotificationType.WARNING);
return false;
}
return true;
}
close - установлен в true, если экран после коммита
будет закрыт.
Реализация метода по умолчанию, если экран не
закрывается, отображает сообщение об успешном
коммите изменений и вызывает метод postInit().
Данный метод можно переопределить в контроллере
для выполнения некоторых действий после успешного
коммита, например:
Далее приведены диаграммы последовательностей
инициализации и различных вариантов коммита экрана
редактирования.
@Inject
private Datasource<Driver> driverDs;
@Inject
private EntitySnapshotService
entitySnapshotService;
@Override
protected boolean postCommit(boolean committed,
boolean close) {
if (committed) {
entitySnapshotService.createSnapshot(driverDs.getIte
m(), driverDs.getView());
}
return super.postCommit(committed, close);
}
Рисунок 16. 16. Инициализация экрана редактирования
Рисунок 17. 17. Коммит и закрытие экрана с фреймом editWindowActions editWindowActions
Рисунок 18. 18. Коммит экрана с фреймом extendedEditWindowActions extendedEditWindowActions
Рисунок 19. 19. Коммит и закрытие экрана с фреймом extendedEditWindowActions extendedEditWindowActions
APIAPI
commit() - commitAndClose() - getItem() - initNewItem() -
postCommit() - postInit() - preCommit() - setItem()
5.5.1.3.5. EntityCombinedScreen5.5.1.3.5. EntityCombinedScreen
EntityCombinedScreen − базовый класс контроллеров
комбинированных экранов, является наследником
AbstractLookup.
Класс EntityCombinedScreen ищет ключевые компоненты
экрана, такие как таблица, eld group и некоторые другие,
по зашитым в код идентификаторам. Если эти
компоненты в вашем экране названы по другому,
переопределите protected-методы класса и возвращайте из
них ваши идентификаторы, чтобы контроллер мог найти
нужные компоненты. См. JavaDocs класса для более
подробной информации.
5.5.1.3.6. 5.5.1.3.6. Инжекция зависимостей контроллеров
В контроллерах можно использовать Dependency Injection
для получения ссылок на используемые объекты. Для
этого нужно объявить либо поле соответствующего типа,
либо метод доступа на запись (setter) с соответствующим
типом результата, и добавить ему одну из следующих
аннотаций:
@Inject - простейший вариант, поиск объекта для
инжекции будет произведен по типу поля/метода и по
имени, эквивалентному имени поля либо имени
атрибута (по правилам JavaBeans) для метода
@Named("someName") - вариант с явным указанием имени
искомого объекта
Инжектировать в контроллеры можно следующие
объекты:
Визуальные компоненты данного экрана, определенные
в XML-дескрипторе. Если тип атрибута унаследован от
Component, в текущем экране будет произведен поиск
компонента с соответствующим именем.
Действия, определенные в XML-дескрипторе - см.
Действия. Интерфейс Action
Источники данных, определенные в XML-дескрипторе.
Если тип атрибута унаследован от Datasource, в
текущем экране будет произведен поиск источника
данных с соответствующим именем.
UserSession. Если тип атрибута - UserSession, будет
инжектирован объект текущей пользовательской
сессии.
DsContext. Если тип атрибута - DsContext, будет
инжектирован DsContext текущего экрана.
WindowContext. Если тип атрибута - WindowContext,
будет инжектирован WindowContext текущего экрана.
DataSupplier. Если тип атрибута - DataSupplier, будет
инжектирован соответствующий экземпляр.
Любой бин, определенный в контексте данного
клиентского блока приложения, в том числе:
импортируемые клиентом сервисы MiddlewareMiddleware
конфигурационные интерфейсы
ComponentsFactory
WindowConfig
ExportDisplay
BackgroundWorker
Если ничего из вышеперечисленного не подошло и
контроллер имеет компаньонов, в случае совпадения
типов будет инжектирован компаньон для текущего
типа клиента.
С помощью специальной аннотации @WindowParam можно
инжектировать в контроллер параметры, передаваемые в
мэп метода init(). Аннотация имеет атрибут name, в
котором указывается имя параметра (ключ в мэп), и
опциональный атрибут required. Если required = true,
то при отсутствии в мэп соответствующего параметра в
лог выводится сообщение с уровнем WARNING.
Пример инжекции объекта типа Job, передаваемого в
метод init() контроллера:
5.5.1.3.7. 5.5.1.3.7. Компаньоны контроллеров
Базовые классы контроллеров расположены в модуле guigui
базового проекта cubacuba и не содержат ссылок на классы
реализации визуальных компонентов (SwingSwing или VaadinVaadin),
что дает возможность использовать их в клиентах обоих
типов.
@WindowParam(name = "job", required = true)
protected Job job;
В то же время конкретные классы контроллеров могут
быть расположены как в модуле guigui, так и в webweb или desktopdesktop,
в зависимости от применяемых в проекте клиентских
блоков и специфики экрана. Если контроллер является
универсальным, но для разных типов клиента требуется
дополнительная функциональность, ее можно
определить в так называемых
классах-компаньонах
.
Класс-компаньон располагается в модуле клиента
соответствующего типа (webweb или desktopdesktop) и реализует
интерфейс, задаваемый в использующем его
контроллере. Класс компаньона задается в элементе
companions XML-дескриптора экрана. Контроллер может
получить ссылку на экземпляр компаньона с помощью
инжекции или вызовом getCompanion(), и в нужный
момент передать ему управление, например, для
дополнительной инициализации визуальных
компонентов специфичным для данного типа клиента
способом.
Например, необходимо раздельно для веб и десктоп
клиентов проинициализировать таблицу некоторого
экрана. Тогда в контроллере экрана, расположенном в
модуле guigui, определяем интерфейс компаньона и
делегируем ему инициализацию таблицы:
public class CustomerBrowse extends AbstractLookup {
public interface Companion {
void initTable(Table<Customer> table);
}
@Inject
protected Table<Customer> table;
@Inject
protected Companion companion;
В модулях webweb и desktopdesktop создаем соответствующие классы
реализации компаньона:
protected Companion companion;
@Override
public void init(Map<String, Object> params) {
if (companion != null) {
companion.initTable(table);
}
}
}
public class WebCustomerBrowseCompanion implements
CustomerBrowse.Companion {
@Override
public void initTable(Table<Customer> table) {
com.vaadin.ui.Table webTable =
(com.vaadin.ui.Table)
WebComponentsHelper.unwrap(table);
// do something specific to Vaadin table
}
}
public class DesktopCustomerBrowseCompanion implements
CustomerBrowse.Companion {
@Override
public void initTable(Table<Customer> table) {
javax.swing.JTable desktopTable =
(javax.swing.JTable)
DesktopComponentsHelper.unwrap(table);
// do something specific to Swing table
}
}
И регистрируем классы реализации компаньона в XML-
дескрипторе экрана:
Так как классы-компаньоны расположены в webweb и desktopdesktop
модулях, в них можно использовать метод unwrap()
классов WebComponentsHelper и DesktopComponentsHelper для
извлечения из интерфейса Table ссылок на реализующие
таблицу Vaadin и Swing компоненты, и работать с ними
непосредственно.
5.5.1.4. Screen Agent5.5.1.4. Screen Agent
Указание агента позволяет выбрать экран в зависимости
от текущего устройства и параметров его дисплея.
Например, можно создать два экрана с различной
компоновкой (и, возможно, различной
функциональностью), и зарегистрировать их в файле
screens.xml с одним идентификатором. Тогда во время
<window ...
class="com.company.sample.gui.customers.CustomerBrowse
">
<companions>
<web
class="com.company.sample.web.customers.WebCustomerBro
wseCompanion"/>
<desktop
class="com.company.sample.desktop.customers.DesktopCus
tomerBrowseCompanion"/>
</companions>
<dsContext>...</dsContext>
<layout>...</layout>
</window>
screens.xml с одним идентификатором. Тогда во время
выполнения платформа выберет экран, который лучше
подходит для дисплея, с которого пользователь работает
с приложением.
В платформе предопределены три агента: DESKTOP,
TABLET, PHONE. Они заданы следующими классами:
DesktopScreenAgent, TabletScreenAgent,
PhoneScreenAgent. В проекте приложения можно
определить собственные агенты путем создания бинов,
реализующих интерфейс ScreenAgent.
Агент указывается для экрана в файле screens.xml.
Значением атрибута agent должна быть либо одна из
вышеперечисленных констант, либо имя бина,
реализующего ScreenAgent.
В Studio агент задается на вкладке PropertiesProperties дизайнера
экранов.
5.5.2. 5.5.2. Библиотека визуальных компонентов
Компоненты
Контейнеры
Разное
5.5.2.1. 5.5.2.1. Компоненты
Меню
AppMenu
SideMenu
Кнопки
Calendar
ColorPicker
FieldGroup
Filter
Image
PopupView
TokenList
5.5.2.1.1. AppMenu5.5.2.1.1. AppMenu
Компонент AppMenu позволяет динамически управлять
элементами главного меню в главном окне приложения.
CUBA Studio предоставляет готовый шаблон главного
экрана на основе стандартного экрана mainWindow
платформы. Шаблон расширяет класс AppMainWindow и
платформы. Шаблон расширяет класс AppMainWindow и
обеспечивает прямой доступ к экземпляру компонента
AppMenu:
Методы интерфейса AppMenu:
addMenuItem() - добавляет элемент меню в конец списка
элементов или на позицию с указанным индексом.
createMenuItem() - фабричный метод для создания
нового элемента меню. Не добавляет элемент к меню.
id должен быть уникальным внутри всего меню.
createSeparator() - создаёт разделитель элементов
меню.
getMenuItem()/getMenuItemNN() - возвращает объект
элемента меню по его идентификатору.
getMenuItems() - возвращает список элементов меню.
public class ExtAppMainWindow extends AppMainWindow {
@Override
public void init(Map<String, Object> params) {
super.init(params);
AppMenu.MenuItem item =
mainMenu.createMenuItem("shop", "Shop");
AppMenu.MenuItem subItem =
mainMenu.createMenuItem("customer", "Customers", null,
menuItem -> {
showNotification("Customers menu item
clicked", NotificationType.HUMANIZED);
});
item.addChildItem(subItem);
mainMenu.addMenuItem(item, 0);
}
}
getMenuItems() - возвращает список элементов меню.
hasMenuItems() - возвращает true, если меню содержит
элементы.
Методы интерфейса MenuItem:
addChildItem() / removeChildItem() -
добавляет/удаляет элемент меню в конец или на
указанную позицию в списке дочерних элементов.
getCaption() - возвращает строковый заголовок
элемента меню.
getChildren() - возвращает список дочерних
элементов.
setCommand() - используется для описания действия,
которое должно быть выполнено при выборе этого
элемента меню кликом мыши.
setDescription() - устанавливает строковое описание
элемента меню, отображаемое в виде всплывающей
подсказки.
setIcon() - устанавливает пиктограмму элемента меню.
getId() - возвращает идентификатор элемента меню.
getMenu() - возвращает родительский экземпляр
AppMenu.
setStylename() - устанавливает один или более
пользовательских стилей для компонента, заменяя все
ранее заданные стили. Имена стилей при перечислении
отделаются пробелами. Имя стиля должно быть
названием существующего CSS-класса.
hasChildren() - возвращает true, если у элемента меню
есть дочерние элементы.
isSeparator() - возвращает true, если элемент
является разделителем.
setVisible() - управляет видимостью элемента меню.
APIAPI
APIAPI
addMenuItem - createMenuItem - createSeparator - setCommand
5.5.2.1.2. BrowserFrame5.5.2.1.2. BrowserFrame
Компонент BrowserFrame предназначен для включения
веб-страницы на страницу приложения. Это аналог HTML-
элемента iframe.
XML-имя компонента: browserFrame
Компонент реализован для блока Web ClientWeb Client.
Пример использования компонента browserFrame в XML-
дескрипторе экрана:
Подобно компоненту Image, BrowserFrame также можно
использовать для отображения графического
содержимого из различных источников. Тип ресурса
<browserFrame id="browserFrame"
height="250px"
width="500px"
align="MIDDLE_CENTER">
<url url="https://doc.cuba-platform.com/manual-
6.6/"/>
</browserFrame>
содержимого из различных источников. Тип ресурса
можно указать декларативно с помощью элементов
browserFrame, перечисленных ниже:
classpath - ресурс, расположенный в classpath.
file - файл с изображением.
relativePath - относительный путь к файлу в каталоге
приложения.
theme - ресурс из темы приложения, например:
url - ресурс, загружаемый по URL.
<browserFrame>
<classpath
path="com/company/sample/web/screens/myPic.jpg"/>
</browserFrame>
<browserFrame>
<file
path="D:\sample\modules\web\web\VAADIN\images\myImag
e.jpg"/>
</browserFrame>
<browserFrame>
<relativePath path="VAADIN/images/myImage.jpg"/>
</browserFrame>
<browserFrame>
<theme
path="../halo/com.company.demo/myPic.jpg"/>
</browserFrame>
Атрибуты browserFrame:
alternateText - устанавливает альтернативный текст
на случай, если ресурс недоступен или не задан.
Параметры ресурсов browserFrame:
bufferSize - размер буфера, используемого для
загрузки этого ресурса, в байтах.
cacheTime - время хранения объекта в кэше в
миллисекундах.
mimeType - MIME-тип ресурса.
<browserFrame>
<url url="http://www.foobar2000.org/"/>
</browserFrame>
<browserFrame>
<file bufferSize="1024" path="C:/img.png"/>
</browserFrame>
<browserFrame>
<file cacheTime="2400" path="C:/img.png"/>
</browserFrame>
<browserFrame>
<url
url="https://avatars3.githubusercontent.com/u/175485
14?v=4&s=200"
mimeType="image/png"/>
</browserFrame>
Методы интерфейса BrowserFrame:
addSourceChangeListener() - добавляет слушатель для
отслеживания изменений источника содержимого.
setSource() - устанавливает источник содержимого
фрейма. Метод принимает тип ресурса и возвращает
объект ресурса, который может быть сконфигурирован
далее. Для каждого типа ресурсов есть свои методы,
например, setPath() для ThemeResource или
setStreamSupplier() для StreamResource:
Вы можете использовать те же типы ресурсов, что и для
компонента Image.
createResource() - создаёт ресурс фрейма указанного
типа. Созданный объект может быть позже передан в
метод setSource():
browserFrame.addSourceChangeListener(event ->
showNotification("Content updated"));
BrowserFrame frame =
componentsFactory.createComponent(BrowserFrame.class
);
try {
frame.setSource(UrlResource.class).setUrl(new
URL("http://www.foobar2000.org/"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
UrlResource resource =
browserFrame.createResource(UrlResource.class)
.setUrl(new URL(fromString));
ДЕМОНСТРАЦИЯ
Атрибуты browserFrame browserFrame
align - alternateText - caption - colspan - description - enable - height
- icon - id - responsive - rowspan - stylename - visible - width
Атрибуты ресурсов browserFrame browserFrame
bufferSize - cacheTime - mimeType
Элементы browserFrame browserFrame
classpath - le - relativePath - theme - url
APIAPI
addSourceChangeListener - createResource - setSource
5.5.2.1.3. Button5.5.2.1.3. Button
Кнопка (Button) − компонент,
обеспечивающий выполнение действия при нажатии.
XML-имя компонента: button
Компонент кнопки реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Кнопка может содержать текст или пиктограмму (или и
то и другое). На рисунке ниже отображены разные виды
кнопок.
Пример кнопки с названием, взятым из пакета
локализованных сообщений, и с всплывающей
подсказкой:
.setUrl(new URL(fromString));
browserFrame.setSource(resource);
подсказкой:
Название кнопки задается с помощью атрибута caption,
всплывающая подсказка − с помощью атрибута description.
Если атрибут disableOnClick имеет значение true,
кнопка будет автоматически отключена после клика по
ней. Обычно это делается для того, чтобы предотвратить
случайные повторные клики по кнопке. Впоследствии, вы
можете снова включить кнопку с помощью вызова метода
setEnabled(true).
Атрибут icon указывает на местоположение пиктограммы
в каталоге темы. Подробную информацию о том, где
следует располагать файлы пиктограмм, можно
прочитать в Создание темы приложения.
Пример создания кнопки с пиктограммой:
Основная функция кнопки − выполнить некоторое
действие при нажатии на нее. Определить метод
контроллера, который будет вызываться при нажатии на
кнопку, можно с помощью атрибута invoke. Значением
атрибута должно быть имя метода контроллера,
удовлетворяющего следующим условиям:
Метод должен быть public.
Метод должен возвращать void.
Метод должен либо не иметь аргументов, либо иметь
один аргумент типа Component. Если метод имеет
аргумент Component, то при вызове в него будет передан
<button id="textButton" caption="msg://someAction"
description="Press me"/>
<button id="iconButton" caption=""
icon="icons/save.png"/>
аргумент Component, то при вызове в него будет передан
экземпляр вызвавшей кнопки.
В качестве примера показано описание кнопки,
вызывающей метод someMethod:
В контроллере экрана необходимо определить метод
someMethod:
Атрибут invoke игнорируется, если для кнопки задан
атрибут action. Атрибут action содержит имя действия,
соответствующего данной кнопке.
Пример кнопки с атрибутом action:
Кнопке можно назначить любое действие, имеющееся в
каком-либо компоненте, реализующем интерфейс
Component.ActionsHolder (это актуально для Table,
GroupTable, TreeTable, Tree). Причем неважно, каким образом
эти действия добавлены - декларативно в XML-
дескрипторе или программно в контроллере. В любом
<button invoke="someMethod"
caption="msg://someButton"/>
public void someMethod() {
//some actions
}
<actions>
<action id="someAction"
caption="msg://someAction"/>
</actions>
<layout>
<button action="someAction"/>
дескрипторе или программно в контроллере. В любом
случае для использования такого действия достаточно в
атрибуте action указать через точку имя компонента и
идентификатор нужного действия. Например, в
следующем примере кнопке назначается действие create
таблицы coloursTable:
Действие для кнопки можно также создавать
программно, в контроллере экрана, используя
наследование от класса BaseAction.
Если для Button установлен экземпляр Action, то кнопка
возьмет из него следующие свои свойства: caption,
description, icon, enable, visible. Свойства caption и
description будут проставлены из действия только в том
случае, если они не установлены в самом Button.
Остальные перечисленные свойства действия имеют
безусловный приоритет над свойствами кнопки. Если
свойства действия меняются уже после установки этого
Action для Button, то соответственно меняться будут и
свойства Button, то есть кнопка слушает изменение
свойств действия. В этом случае меняется и свойства
caption и description, причем даже если они изначально
были назначены на саму кнопку.
Стили компонента Button Button
В веб-клиенте с темой, основанной на Halo, к компоненту
Button можно применить предопределенные стили.
Стили задаются в XML-дексрипторе или контроллере
экрана с помощью атрибута stylename:
<button action="coloursTable.create"/>
<button id="button"
caption="Friendly button"
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
BUTTON_:
borderless - кнопка без полей.
borderless-colored - кнопка без полей с цветной
надписью.
danger - выделенная кнопка, обозначающая действие,
потенциально небезопасное для пользователя
(которое может вызвать потерю данных и прочие
необратимые изменения).
friendly - выделенная кнопка, обозначающая
предпочтительное действие, безопасное для
пользователя (не вызывающее потери данных и
прочих необратимых изменений).
icon-align-right - выравнивание пиктограммы по
правому краю надписи.
icon-align-top - расположение пиктограммы над
надписью.
icon-only - отображается только пиктограмма, кнопка
квадратной формы.
primary - кнопка основного действия (т.е.кнопка,
которая получает фокус при нажатии кнопки Enter в
форме ввода). Используйте внимательно, не более
одной основной кнопки на представление.
quiet - "незаметная" кнопка, поля которой не видны до
наведения указателя мыши.
stylename="friendly"/>
button.setStyleName(HaloTheme.BUTTON_FRIENDLY);
ДЕМОНСТРАЦИЯ
Атрибуты button button
action - align - caption - description - disableOnClick - enable - icon -
id - invoke - stylename - tabIndex - visible - width
Предопределенные стили button button
borderless - borderless-colored - danger - friendly - huge - icon-align-
right - icon-align-top - icon-only - large - primary - quiet - small - tiny
5.5.2.1.4. BulkEditor5.5.2.1.4. BulkEditor
BulkEditor - компонент, позволяющий
менять значения атрибутов сразу нескольких выбранных
экземпляров сущностей. Компонент представляет собой
кнопку, добавляющуюся к таблице или дереву и при
нажатии открывающую редактор сущностей.
XML-имя компонента: bulkEditor
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Для использования BulkEditor у таблицы или дерева
должен быть задан атрибут multiselect="true".
Экран редактирования сущностей генерируется
автоматически на основе заданного представления
(содержащего только поля данной сущности, в том числе
ссылки) и разрешений пользователя. Системные
атрибуты в редакторе также не отображаются.
Атрибуты сущности в редакторе сортируются по
алфавиту. По умолчанию они пусты. При коммите экрана
заданные на экране непустые значения атрибутов
проставляются всем выбранным экземплярам сущности.
Редактор позволяет удалить значение определенного
поля в БД у всех выбранных сущностей, установив его в
поля в БД у всех выбранных сущностей, установив его в
null. Для этого необходимо нажать на кнопку рядом
с соответствующим полем. После этого поле становится
нередактируемым. Разблокировать поле можно, нажав на
кнопку эту же кнопку снова.
Пример описания компонента bulkEditor для таблицы:
Атрибут for является обязательным. В нем указывается
идентификатор таблицы или дерева, в данном случае -
invoiceTable.
Атрибут exclude может содержать регулярное
выражения для явного исключения определенных полей
из списка редактируемых. Например: date|customer
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<!-- ... -->
</actions>
<buttonsPanel>
<!-- ... -->
<bulkEditor for="invoiceTable"
exclude="customer"/>
</buttonsPanel>
ДЕМОНСТРАЦИЯ
Атрибуты bulkEditor bulkEditor
align - caption - description - enable - exclude - for - icon - id -
openType - stylename - tabIndex - visible - width
5.5.2.1.5. Calendar5.5.2.1.5. Calendar
Компонент Calendar предназначен для
организации и отображения событий календаря.
XML-имя компонента: calendar.
Компонент реализован для блока Web ClientWeb Client.
Пример описания компонента в XML-дескрипторе экрана:
<calendar id="calendar"
Режим отображения определяется временным
диапазоном календаря, который задаётся его начальной
и конечной датой. По умолчанию используется режим
отображения недели, работающий с диапазонами до семи
дней. Для отображения календаря на один день
используйте диапазон в пределах одной календарной
даты. Режим отображения месяца применяется, если
заданный диапазон превышает одну неделю (семь дней).
Кнопки навигации для перелистывания календаря на
одну неделю вперёд/назад по умолчанию отключены.
Чтобы кнопки были видны в режиме отображения недели,
используйте атрибут navigationButtonsVisible:
<calendar id="calendar"
captionProperty="caption"
startDate="2016-10-01"
endDate="2016-10-31"
height="100%"
width="100%"/>
<calendar width="100%"
height="100%"
navigationButtonsVisible="true"/>
Атрибуты calendar:
endDate - конечная дата диапазона календаря.
endDateProperty - имя атрибута сущности, содержащего
конечную дату события.
descriptionProperty - имя атрибута сущности,
содержащего описание события.
isAllDayProperty - имя атрибута сущности,
отвечающего за отображение события в течение всего
дня.
startDate - начальная дата диапазона календаря.
startDateProperty - имя атрибута сущности,
содержащего начальную дату события.
stylenameProperty - имя атрибута сущности,
содержащего имя стиля события.
timeFormat - формат времени: 12H or 24H.
Для отображения событий в ячейках календаря их можно
прямо добавлять в объект Calendar при помощи метода
addEvent() или использовать интерфейс
CalendarEventProvider. Пример добавления события
напрямую:
@Inject
private Calendar calendar;
public void generateEvent(String caption, String
description, Date start, Date end, boolean isAllDay,
String stylename) {
SimpleCalendarEvent calendarEvent = new
SimpleCalendarEvent();
calendarEvent.setCaption(caption);
calendarEvent.setDescription(description);
calendarEvent.setStart(start);
Интерфейс CalendarEventProvider имеет две готовые
реализации: ListCalendarEventProvider (создаваемый по
умолчанию) и EntityCalendarEventProvider.
ListCalendarEventProvider заполняется данными с
помощью метода addEvent(), принимающего объект
CalendarEvent в качестве параметра:
calendarEvent.setStart(start);
calendarEvent.setEnd(end);
calendarEvent.setAllDay(isAllDay);
calendarEvent.setStyleName(stylename);
calendar.getEventProvider().addEvent(calendarEvent);
}
@Inject
private Calendar calendar;
public void addEvents() {
ListCalendarEventProvider
listCalendarEventProvider = new
ListCalendarEventProvider();
calendar.setEventProvider(listCalendarEventProvider);
listCalendarEventProvider.addEvent(generateEvent(
"Training", "Student training", "2016-10-
17 09:00", "2016-10-17 14:00", false, "event-blue"));
listCalendarEventProvider.addEvent(generateEvent(
"Development", "Platform development",
"2016-10-17 15:00", "2016-10-17 18:00", false, "event-
red"));
listCalendarEventProvider.addEvent(generateEvent(
"Party", "Party with friends", "2016-10-22
13:00", "2016-10-22 18:00", false, "event-yellow"));
}
EntityCalendarEventProvider получает данные
напрямую из атрибутов сущности. Чтобы
EntityCalendarEventProvider мог использовать
сущность, она должна иметь как минимум следующие
атрибуты: дата начала события (тип DateTime), дата
окончания события (тип DateTime) и заголовок события
(тип String).
В следующем примере мы предположим, что сущность в
источнике данных имеет все необходимые атрибуты:
eventCaption, eventDescription, eventStartDate,
eventEndDate, eventStylename, и укажем их имена в
качестве значений атрибутов calendar:
private SimpleCalendarEvent generateEvent(String
caption, String description, String start, String end,
Boolean allDay, String style) {
SimpleCalendarEvent calendarEvent = new
SimpleCalendarEvent();
SimpleDateFormat df = new SimpleDateFormat("yyyy-
MM-dd HH:mm");
calendarEvent.setCaption(caption);
calendarEvent.setDescription(description);
calendarEvent.setStart(df.parse(start));
calendarEvent.setEnd(df.parse(end));
calendarEvent.setAllDay(allDay);
calendarEvent.setStyleName(style);
return calendarEvent;
}
<calendar id="calendar"
datasource="calendarEventsDs"
width="100%"
height="100%"
startDate="2016-10-01"
Для пользовательского взаимодействия с элементами
Calendar, такими как подписи даты и номера недель,
выбор диапазона даты/времени, перетаскивание событий
и изменение их размера, могут быть заданы различные
слушатели. Слушатели также используются для кнопок
навигации, листающих диапазон календаря вперёд и
назад. Ниже приведён список слушателей по умолчанию:
addDateClickListener(CalendarDateClickListener
listener); - добавляет слушатель кликов по дате.
addWeekClickListener() - добавляет слушатель кликов
по номеру недели.
addEventClickListener() - добавляет слушатель
кликов по событию календаря.
addEventResizeListener() - добавляет слушатель
изменения размеров события календаря.
addEventMoveListener() - добавляет слушатель
перетаскивания события.
startDate="2016-10-01"
endDate="2016-10-31"
captionProperty="eventCaption"
descriptionProperty="eventDescription"
startDateProperty="eventStartDate"
endDateProperty="eventEndDate"
stylenameProperty="eventStylename"/>
calendar.addDateClickListener(
calendarDateClickEvent ->
showNotification(String.format("Date
clicked: %s",
calendarDateClickEvent.getDate().toString()),
NotificationType.HUMANIZED));
перетаскивания события.
addForwardClickListener() - добавляет слушатель
перелистывания календаря вперёд во времени.
addBackwardClickListener() - добавляет слушатель
перелистывания календаря назад во времени.
addRangeSelectListener() - добавляет слушатель
выбора диапазона календаря.
Событиям календаря можно задавать стили с помощью
CSS. Для настройки стиля задайте имя стиля и его
параметры в файле .scss. Пример настройки цвета фона
события:
Затем вызовите метод setStyleName для нужного
события:
В результате, цвет фона события стал зелёным:
.v-calendar-event.event-green {
background-color: #c8f4c9;
color: #00e026;
}
calendarEvent.setStyleName("event-green");
ДЕМОНСТРАЦИЯ
Для компонента Calendar можно изменить названия дней
недели и месяцев по умолчанию, используя методы
setDayNames() и setMonthNames():
Атрибуты calendar calendar
caption - captionProperty - colspan - datasource - description -
descriptionProperty - endDateProperty - endDate - height - icon - id -
isAllDayProperty - rowspan - startDate - startDateProperty -
stylename - stylenameProperty - timeFormat - visible - width
APIAPI
addEvent - setEventProvider - setDayNames
Слушатели calendar calendar
CalendarBackwardClickListener - CalendarDateClickListener -
CalendarEventClickListener - CalendarEventMoveListener -
CalendarEventResizeListener - CalendarForwardClickListener -
CalendarRangeSelectListener - CalendarWeekClickListener
5.5.2.1.6. CheckBox5.5.2.1.6. CheckBox
Флажок (CheckBox) − компонент,
Map<DayOfWeek, String> days = new HashMap<>(7);
days.put(DayOfWeek.MONDAY,"Heavens and earth");
days.put(DayOfWeek.TUESDAY,"Sky");
days.put(DayOfWeek.WEDNESDAY,"Dry land");
days.put(DayOfWeek.THURSDAY,"Stars");
days.put(DayOfWeek.FRIDAY,"Fish and birds");
days.put(DayOfWeek.SATURDAY,"Animals and man");
days.put(DayOfWeek.SUNDAY,"Rest");
calendar.setDayNames(days);
ДЕМОНСТРАЦИЯ
Флажок (CheckBox) − компонент,
имеющий два состояния: выбран, не выбран.
XML-имя компонента: checkBox.
Компонент CheckBox реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Пример флажка с надписью, взятой из пакета
локализованных сообщений:
Сброс или установка флажка изменяет его значение:
Boolean.TRUE или Boolean.FALSE. Значение может быть
получено с помощью метода getValue() и установлено с
помощью метода setValue(). Если в setValue() передать
null, то устанавливается значение Boolean.FALSE и
флажок снимается.
Изменение значения флажка, так же как и любого другого
компонента, реализующего интерфейс Field, можно
отслеживать с помощью слушателя ValueChangeListener.
Например:
<checkBox id="accessField"
caption="msg://accessFieldCaption"/>
@Inject
private CheckBox accessField;
@Override
public void init(Map<String, Object> params) {
accessField.addValueChangeListener(event -> {
if (Boolean.TRUE.equals(event.getValue())) {
showNotification("set",
NotificationType.HUMANIZED);
Для создания флажка, связанного с данными, необходимо
использовать атрибуты datasource и property.
Как видно из примера, в экране описывается источник
данных customerDs для некоторой сущности Покупатель
(Customer), имеющей атрибут active. В компоненте
checkBox в атрибуте datasource указывается ссылка на
источник данных, а в атрибуте property − название
атрибута сущности, значение которого должно быть
отображено флажком. Атрибут должен быть типа
Boolean. Значением атрибута может быть null, при этом
флажок снимается.
Атрибуты checkBox checkBox
align - caption - datasource - description - editable - enable - height -
icon - id - property - stylename - tabIndex - visible - width
APIAPI
addValueChangeListener commit - discard - isModied
} else {
showNotification("not set",
NotificationType.HUMANIZED);
}
});
}
<dsContext>
<datasource id="customerDs"
class="com.sample.sales.entity.Customer"
view="_local"/>
</dsContext>
<layout>
<checkBox datasource="customerDs"
property="active"/>
ДЕМОНСТРАЦИЯ
addValueChangeListener commit - discard - isModied
5.5.2.1.7. ColorPicker5.5.2.1.7. ColorPicker
ColorPicker представляет собой поле
для предпросмотра и выбора цвета. Компонент
возвращает шестнадцатеричный (HEX) код цвета в виде
строки.
Пример использования ColorPicker с надписью, взятой из
пакета локализованных сообщений:
Пример ColorPicker с закрытым окном палитры.
Для создания ColorPicker, связанного с данными,
необходимо использовать атрибуты datasource и property.
Атрибуты colorPicker:
buttonCaption - надпись кнопки компонента.
defaultCaptionEnabled - если установлено true и не
задан атрибут buttonCaption, в качестве надписи
кнопки используется HEX-код текущего цвета.
historyVisible - определяет видимость истории
последних выбранных цветов в окне палитры.
Видимость вкладок окна палитры можно определить с
помощью атрибутов:
swatchesVisible - определяет видимость вкладки
палитры.
rgbVisible - определяет видимость вкладки селектора
RGB.
hsvVisible - определяет видимость вкладки селектора
<colorPicker id="colorPicker"
caption="msg://colorPickerCaption"/>
<dsContext>
<datasource id="carsDs"
class="com.sample.sales.entity.Cars" view="_local"/>
</dsContext>
<layout>
<colorPicker id="colorPicker" datasource="carsDs"
property="color"/>
hsvVisible - определяет видимость вкладки селектора
HSV.
По умолчанию включена только вкладка селектора RGB.
Надписи окна палитры можно переопределить:
popupCaption - надпись заголовка окна палитры.
confirmButtonCaption - надпись кнопки
подтверждения.
cancelButtonCaption - надпись кнопки отмены.
swatchesTabCaption - заголовок вкладки палитры.
lookupAllCaption - надпись элемента выпадающего
списка, отвечающего за все цвета.
lookupRedCaption - надпись элемента выпадающего
списка, отвечающего за оттенки красного.
lookupGreenCaption - надпись элемента выпадающего
списка, отвечающего за оттенки зеленого.
lookupBlueCaption - надпись элемента выпадающего
списка, отвечающего за оттенки синего.
Метод компонента getValue() возвращает строку,
содержащую HEX-код цвета.
Атрибуты colorPicker colorPicker
align - buttonCaption - cancelButtonCaption - caption -
conrmButtonCaption - datasource - defaultCaptionEnabled -
editable - height - historyVisible - hsvVisible - icon - id -
lookupAllCaption - lookupBlueCaption - lookupGreenCaption -
lookupRedCaption - popupCaption - rgbVisible - required -
stylename - swatchesTabCaption - swatchesVisible - tabIndex -
visible - width
APIAPI
ValueChangeListener
5.5.2.1.8. CurrencyField5.5.2.1.8. CurrencyField
5.5.2.1.8. CurrencyField5.5.2.1.8. CurrencyField
CurrencyField - это разновидность текстового поля,
предназначенная для ввода денежных единиц. Поле
содержит ярлык с обозначением валюты и по умолчанию
имеет выравнивание по правому краю.
XML-имя компонента: currencyField.
Компонент CurrencyField реализован только для блока
Web ClientWeb Client.
CurrencyField в основном повторяет функциональность
TextField: вы так же можете указать тип данных для поля,
за исключением того, что CurrencyField поддерживает
только числовые типы данных, унаследованные от
NumericDatatype. Если установлен иной тип данных,
будет выброшено исключение.
CurrencyField можно привязать к источнику данных с
помощью атрибутов datasource и property:
Компонент currencyField имеет следующие
специфические атрибуты:
currency - текст, который будет отображаться в ярлыке
валюты.
currencyLabelPosition - определяет положение ярлыка
внутри текстового поля:
<currencyField currency="$"
datasource="orderDs"
property="amount"/>
<currencyField currency="USD"/>
ДЕМОНСТРАЦИЯ
внутри текстового поля:
LEFT - слева от поля ввода,
RIGHT - справа от поля ввода (значение по умолчанию).
showCurrencyLabel - управляет видимостью ярлыка со
значком валюты.
Атрибуты currencyField currencyField
align - caption - colspan - currency - currencyLabelPosition -
datasource - datatype - description - editable - enable - height - icon
- id - property - required - requiredMessage - rowspan -
showCurrencyLabel - stylename - visible - width
Предопределенные стили currencyField currencyField
huge - large - small - tiny
APIAPI
addValidator - addValueChangeListener - commit - discard -
isModied - setCurrency - setCurrencyLabelPosition -
setShowCurrencyLabel
5.5.2.1.9. DataGrid5.5.2.1.9. DataGrid
DataGrid, подобно компоненту Table,
позволяет выводить информацию в виде таблицы,
сортировать её, вызывать действия для выбранных строк,
а также более эффективно управлять строками и
колонками таблицы за счёт отложенной загрузки данных
при прокрутке.
XML-имя компонента: dataGrid.
Компонент реализован для блока Web ClientWeb Client.
Пример описания компонента в XML-дескрипторе экрана:
В данном примере атрибут id - это идентификатор
колонки, а атрибут property содержит имя атрибута
сущности, содержащейся в источнике данных, который
следует использовать в качестве данных для колонки.
<dsContext>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order"
view="orderWithCustomer">
<query>
select o from sales$Order o order by
o.date
</query>
</collectionDatasource>
</dsContext>
<dataGrid id="ordersDataGrid"
datasource="ordersDs"
height="100%"
width="100%">
<columns>
<column id="date" property="date"/>
<column id="customerName"
property="customer.name"/>
<column id="amount" property="amount"/>
</columns>
</dataGrid>
следует использовать в качестве данных для колонки.
Элементы dataGrid: dataGrid:
columns - обязательный элемент, определяет набор
колонок DataGrid. Каждая колонка описывается во
вложенном элементе column со следующими
атрибутами:
id - необязательный атрибут, содержит строковый
идентификатор колонки. Если не задан, в качестве
идентификатора колонки будет использоваться
строковое значение атрибута property. В этом случае
проставление атрибута property является
обязательным, в противном случае будет брошено
исключение GuiDevelopmentException. Атрибут id по-
прежнему является обязательным для колонки,
создаваемой программно.
property - необязательный атрибут, содержит название
атрибута сущности, выводимого в колонке. Может быть
как непосредственным атрибутом сущности,
находящейся в источнике данных, так и атрибутом
связанной сущности; переход по графу объектов
обозначается точкой. Например:
caption - необязательный атрибут, содержит заголовок
колонки. Если не задан, будет отображено
локализованное название атрибута сущности.
<columns>
<column id="date" property="date"/>
<column id="customer" property="customer"/>
<column id="customerName"
property="customer.name"/>
<column id="customerCountry"
property="customer.address.country"/>
</columns>
локализованное название атрибута сущности.
expandRatio - необязательный атрибут, устанавливает
соотношение, с которым столбец расширяется. По
умолчанию все колонки расширяются равномерно
(словно все колонки имеют expandRatio = 1). Если хотя
бы одной колонке установлено иное значение, все
неявные значения удаляются и учитываются только
проставленные.
collapsible - необязательный атрибут, определяющий,
может ли пользователь управлять отображением
колонок с помощью меню (sidebar menu) в правой
верхней части DataGrid. По умолчанию имеет значение
true.
collapsed - необязательный атрибут, при указании true
колонка будет изначально скрыта. По умолчанию имеет
значение false.
collapsingToggleCaption - необязательный атрибут,
задает имя колонки в меню в правой верхней части
DataGrid. По умолчанию имеет значение null, и в этом
случае берется значение из заголовка колонки,
доступного из свойства caption.
resizable - необязательный атрибут, определяет,
может ли пользователь изменять размер колонки.
sortable - необязательный атрибут, позволяющий
запретить сортировку колонки. Вступает в действие,
если атрибут sortable всего DataGrid установлен в
true (что имеет место по умолчанию).
width - необязательный атрибут, отвечает за
изначальную ширину колонки. Может принимать
только числовые значения в пикселах.
minimumWidth - необязательный атрибут, отвечает за
минимальную ширину колонки. Может принимать
только числовые значения в пикселах.
maximumWidth - необязательный атрибут, отвечает за
максимальную ширину колонки. Может принимать
только числовые значения в пикселах.
Элемент column может содержать вложенный элемент
formatter для представления значения атрибута в виде,
отличном от стандартного для данного DataType:
actions - необязательный элемент для описания
действий, связанных с DataGrid. Кроме описания
произвольных действий, поддерживаются следующие
стандартные действия, определяемые перечислением
ListActionType: create, edit, remove, refresh, add, exclude.
buttonsPanel - необязательный элемент, создающий
над DataGrid контейнер ButtonsPanel для отображения
кнопок действий.
rowsCount - необязательный элемент, создающий для
<column id="date">
<formatter
class="com.haulmont.cuba.gui.components.formatters.D
ateFormatter"
format="yyyy-MM-dd HH:mm:ss"/>
</column>
rowsCount - необязательный элемент, создающий для
DataGrid компонент RowsCount, который позволяет
загружать в DataGrid данные постранично. Размер
страницы задается путем ограничения количества
записей в источнике данных методом
CollectionDatasource.setMaxResults() в контроллере
экрана. Также можно управлять количеством записей.
используя универсальный компонент Filter, связанный
с источником данных DataGrid.
Компонент RowsCount может также отобразить общее
число записей, возвращаемых текущим запросом в
источнике данных, без извлечения этих записей. Для
этого при щелчке пользователя на знаке "??" он вызывает
метод AbstractCollectionDatasource.getCount(), что
приводит к выполнению в БД запроса с такими же, как у
текущего запроса, условиями, но с агрегатной функцией
COUNT(*) вместо результатов. Полученное число
отображается вместо знака "??".
Атрибуты dataGrid: dataGrid:
columnResizeMode - устанавливает режим изменения
размера колонок пользователем. Поддерживаются
следующие режимы (по умолчанию ANIMATED):
AMINATED - размер колонки меняется сразу вслед за
курсором.
SIMPLE - размер колонки меняется только после того
как курсор будет отпущен.
Изменение размера колонок можно отслеживать с
помощью слушателя ColumnResizeListener.
columnsCollapsingAllowed - разрешает или запрещает
пользователю скрывать колонки с помощью меню
(sidebar menu) в правой части шапки DataGrid. Флажками
в меню отмечаются отображаемые в данный момент
колонки. В момент установки перезаписывает значение
collapsed каждой отдельной колонки. Установка
значения в false не позволяет атрибуту collapsed
отдельной колонки принять значение true.
Скрытие и отображение колонок можно отслеживать с
помощью слушателя ColumnCollapsingChangeListener.
contextMenuEnabled - включает или выключает
контекстное меню в DataGrid. По умолчанию имеет
значение true.
Щелчки правой кнопкой мыши по области компонента
DataGrid можно отслеживать с помощью слушателя
ContextClickListener.
editorBuffered - включает буферизацию в режиме
внутристрочного редатирования. По умолчанию
буферизация разрешена (true).
editorCancelCaption - устанавливает заголовок кнопки
отмены в режиме редактирования DataGrid.
editorEnabled - включает отображение UI для
внутристрочного редактирования ячеек. Если dataGrid
привязан к источнику данных с типом
ValueCollectionDatasource, предполагается, что он
используется только для чтения, и использование
атрибута editorEnabled в этом случае бессмысленно.
editorSaveCaption - устанавливает заголовок кнопки
сохранения изменений в режиме редактирования
DataGrid.
frozenColumnCount - устанавливает количество
фиксированных колонок в DataGrid. Значение 0
означает, что фиксированных колонок не будет, кроме
встроенной колонки с чекбоксами для множественного
выбора, если она используется. Значение -1 означает,
что фиксированных колонок не будет вообще.
headerVisible - определяет видимость заголовка
DataGrid. По умолчанию имеет значение true.
reorderingAllowed - разрешает или запрещает
пользователю менять местами колонки, перетаскивая
их с помощью мыши. По умолчанию имеет значение
true.
Изменение расположения колонок можно отслеживать
с помощью слушателя ColumnReorderListener.
selectionMode - определяет режим выделения строк.
Поддерживаются следующие режимы:
SINGLE - единичный выбор строки.
MULTI - множественный выбор строк как в таблице.
MULTI_CHECK - множественный выбор строк с
использованием встроенной колонки с чекбоксами.
NONE - выбор строк отключен.
Выделение строк можно отслеживать с помощью
слушателя SelectionListener.
sortable - разрешает или запрещает сортировку в
DataGrid. По умолчанию имеет значение true. Если
сортировка разрешена, то при нажатии на название
колонки справа от названия появляется
соответствующий значок. Сортировку некоторой
отдельной колонки можно запретить с помощью
атрибута sortable этой колонки.
События сортировки DataGrid можно отслеживать с
помощью слушателя SortListener.
помощью слушателя SortListener.
textSelectionEnabled - разрешает или запрещает
выделение текста в ячейках DataGrid. По умолчанию
имеет значение false.
Методы интерфейса DataGrid: DataGrid:
getColumns() - возвращет текущий набор колонок
DataGrid в порядке их текущего отображения.
getSelected(), getSingleSelected() - возвращают
экземпляры сущностей, соответствующие выделенным
в таблице строкам. Коллекцию можно получить
вызовом метода getSelected(). Если ничего не выбрано,
возвращается пустой набор. Если установлен
SelectionMode.SINGLE, удобно пользоваться методом
getSingleSelected(), возвращающим одну выбранную
сущность или null, если ничего не выбрано.
getVisibleColumns() - возвращет набор видимых
колонок DataGrid в порядке их текущего отображения.
scrollTo() - позволяет программно прокрутить
DataGrid до нужной записи. Метод принимает
экземпляр сущности, определяющий нужную строку в
DataGrid. Перегруженный метод, помимо сущности,
принимает ScrollDestination, имеющий следующие
возможные значения:
ANY - прокрутить как можно меньше, чтобы показать
нужную запись.
START - прокрутить так, чтобы нужная запись оказалась
в начале видимой области DataGrid.
MIDDLE - прокрутить так, чтобы нужная запись
оказалась в центре видимой области DataGrid.
END - прокрутить так, чтобы нужная запись оказалась в
конце видимой области DataGrid.
scrollToStart() and scrollToEnd() - позволяют
прокрутить DataGrid в начало и конец соотвественно.
прокрутить DataGrid в начало и конец соотвественно.
setCellStyleProvider() - позволяет задать стиль
отображения ячеек DataGrid.
setRowStyleProvider() - позволяет задать стиль
отображения строк DataGrid.
setEnterPressAction() - позволяет задать действие,
выполняемое при нажатии клавиши EnterEnter. Если такое
действие не задано, таблица пытается найти среди
своих действий подходящее в следующем порядке:
действие, назначенное методом setItemClickAction().
действие, назначенное на клавишу EnterEnter посредством
свойства shortcut.
действие с именем edit.
действие с именем view.
Если такое действие найдено и имеет свойство enabled
= true, оно выполняется.
setItemClickAction() - позволяет задать действие,
выполняемое при двойном клике на строке таблицы.
Если такое действие не задано, при двойном клике
таблица пытается найти среди своих действий
подходящее в следующем порядке:
действие, назначенное на клавишу EnterEnter посредством
свойства shortcut.
действие с именем edit.
действие с именем view.
Если такое действие найдено и имеет свойство enabled
= true, оно выполняется.
События клика по элементу DataGrid можно
отслеживать с помощью слушателя ItemClickListener.
sort() - сортирует данные в переданной колонке в
направлении, заданном одним из двух доступных
значений перечисления SortDirection:
значений перечисления SortDirection:
ASCENDING - сортировка по возрастанию (например, A-Z,
1..9).
DESCENDING - сортировка по убыванию (например, Z-A,
9..1).
setCellDescriptionProvider() - принимает экземпляр
CellDescriptionProvider, который будет
использоваться для генерации всплывающих подсказок
для отдельных ячеек DataGrid. Строка описания может
содержать HTML-разметку.
setRowDescriptionProvider() - принимает экземпляр
RowDescriptionProvider, который будет
использоваться для генерации всплывающих подсказок
для строк DataGrid. Если CellDescriptionProvider также
установлен, подсказка, сгенерированная
RowDescriptionProvider, будет использована только
для тех ячеек, для которых не задана подсказка ячейки.
customersDataGrid.setRowDescriptionProvider(Instance
::getInstanceName);
customersDataGrid.setCellDescriptionProvider((entity
,columnId)->{
if
("name".equals(columnId)||"lastName".equals(columnId
Использование режима внутристрочного
редактирования::
У компонента DataGrid есть API, позволяющий напрямую
редактировать записи в ячейках. Во время
редактирования ячейки будет отображён UI с кнопками
("name".equals(columnId)||"lastName".equals(columnId
)){
return null;
}
String description="<strong>"+
messages.getTools().getPropertyCaption(entity.getMet
aClass(),columnId)+
": </strong>";
if ("grade".equals(columnId)){
description +=
messages.getMessage(entity.getGrade());
} else if ("active".equals(columnId)){
description += getMessage(entity.getActive()
? "trueString":"falseString");
} else {
description += entity.getValue(columnId);
}
return description;
});
редактирования ячейки будет отображён UI с кнопками
для сохранения и отмены изменений.
Методы API встроенного редактора:
getEditedItemId() - возвращает id редактируемой
записи.
isEditorActive() - возвращает true, если в момент
вызова редактируется какая-либо запись.
editItem() - открывает интерфейс внутристрочного
редактора для указанной записи. Пролистывает
таблицу до нужной записи, если в момент вызова она не
была видна на экране.
Вы также можете добавить к встроенному редактору или
удалить слушатели, использовав следующие методы:
addEditorOpenListener(),
removeEditorCloseListener() - слушатель открытия
встроенного редактора DataGrid.
Данный слушатель обрабатывает событие открытия
встроенного редактора DataGrid по двойному щелчку и
позволяет получить доступ к полям редактируемой
строки. Это даёт возможность обновлять значения в
отдельных полях в зависимости от изменения значений
в других полях, не закрывая встроенный редактор.
Например:
customersTable.addEditorOpenListener(event -> {
Map<String, Field> fieldMap = event.getFields();
Field active = fieldMap.get("active");
Field grade = fieldMap.get("grade");
ValueChangeListener listener = e ->
active.setValue(true);
grade.addValueChangeListener(listener);
});
addEditorCloseListener(),
removeEditorCloseListener() - слушатель закрытия
встроенного редактора DataGrid.
addEditorPreCommitListener(),
removeEditorPreCommitListener() - слушатель
редактора DataGrid, срабатывающий в процессе
коммита изменений.
addEditorPostCommitListener(),
removeEditorPostCommitListener() - слушатель,
срабатывающий на финальной стадии коммита
изменений.
Коммит изменений сохраняет их в источнике данных.
Логику сохранения изменений в базу данных необходимо
задать отдельно.
Само поле редактирования также может быть изменено с
помощью интерфейса ColumnEditorFieldGenerator.
Используйте метод setEditorFieldGenerator() для
определённой колонки таблицы, чтобы указать
компонент для отображения в режиме редактирования
этой колонки:
});
ordersGrid.getColumnNN("amount").setEditorFieldGenerat
or((datasource, property) -> {
LookupField lookupField =
componentsFactory.createComponent(LookupField.class);
lookupField.setDatasource(datasource, property);
lookupField.setOptionsList(Arrays.asList(BigDecimal.ZE
RO, BigDecimal.ONE, BigDecimal.TEN));
return lookupField;
});
Результат:
Использование интерфейса ColumnGenerator: ColumnGenerator:
DataGrd имеет возможность добавлять генерируемые,
или высчитываемые, колонки. Для этого существует два
метода:
addGeneratedColumn(String columnId,
ColumnGenerator generator)
addGeneratedColumn(String columnId,
ColumnGenerator generator, int index)
ColumnGenerator - это специальный интерфейс, который
описывает генерируемую колонку:
значение для каждой строки колонки,
тип значения - общий для всей колонки.
Например, для добавления генерируемой колонки,
которая будет отображать логин пользователя в верхнем
регистре, можно использовать следующий код:
});
@Override
public void init(Map<String, Object> params){
DataGrid.Column column =
usersGrid.addGeneratedColumn("loginUpperCase",new
DataGrid.ColumnGenerator<User, String>(){
@Override
public String
getValue(DataGrid.ColumnGeneratorEvent<User> event){
Результат:
ColumnGeneratorEvent, который передается в getValue,
хранит информацио о сущности, которая отображется в
текущей строке DataGrid, и propertyId колонки.
По умолчанию, генерируемая колонка добавляется в
конец таблицы. Управлять расположением генерируемых
колонок можно либо вставляя колонку по индексу, либо
предварительно добавив колонку в XML с id, который
потом передавать в метод addGeneratedColumn.
Использование рендереров::
Отображение данных в колонках может быть изменено с
помощью рендереров. Предположим, что нам необходимо
показывать изображение в строке. Тогда текстовое
значение пути до изображения можно отобразить в виде
изображения с помощью ImageRenderer:
getValue(DataGrid.ColumnGeneratorEvent<User> event){
return
event.getItem().getLogin().toUpperCase();
}
@Override
public Class<String> getType(){
return String.class;
}
},1);
column.setCaption("Login Upper Case");
}
@Override
Результат:
Интерфейс WebComponentRenderer позволяет настроить
отображение веб-компонентов различных типов в
ячейках DataGrid. Интерфейс реализован только для
блока Web ModuleWeb Module. Ниже приведён пример создания
колонки для отображения компонента LookupField:
@Override
public void init(Map<String, Object> params){
DataGrid.Column avatar =
usersGrid.addGeneratedColumn("userAvatar", new
DataGrid.ColumnGenerator<User, String>() {
@Override
public String
getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return "icons/user.png";
}
@Override
public Class<String> getType() {
return String.class;
}
}, 0);
avatar.setCaption("Avatar");
avatar.setRenderer(usersGrid.createRenderer(DataGrid.I
mageRenderer.class));
}
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.global.GlobalConfig;
import
com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.DataGrid;
import com.haulmont.cuba.gui.components.LookupField;
import
com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.security.entity.User;
import
com.haulmont.cuba.web.gui.components.renderers.WebComp
onentRenderer;
import javax.inject.Inject;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
public class Users extends AbstractWindow {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Configuration configuration;
@Inject
private DataGrid<User> usersGrid;
@Override
public void init(Map<String, Object> params) {
Map<String, Locale> locales =
configuration.getConfig(GlobalConfig.class).getAvailab
leLocales();
Map<String, Object> options = new TreeMap<>();
for (Map.Entry<String, Locale> entry :
locales.entrySet()) {
options.put(entry.getKey(),
messages.getTools().localeToString(entry.getValue()));
messages.getTools().localeToString(entry.getValue()));
}
DataGrid.Column column =
usersGrid.addGeneratedColumn("language",
new DataGrid.ColumnGenerator<User,
Component>() {
@Override
public Component
getValue(DataGrid.ColumnGeneratorEvent<User> event) {
LookupField component =
componentsFactory.createComponent(LookupField.class);
component.setOptionsMap(options);
component.setWidth("100%");
User user = event.getItem();
component.setValue(user.getLanguage());
component.addValueChangeListener(e ->
user.setLanguage((String) e.getValue()));
return component;
}
@Override
public Class<Component> getType()
{
return Component.class;
}
});
column.setRenderer(new
WebComponentRenderer());
}
Результат:
Когда тип поля не совпадает с типом данных,
принимаемых рендерером, удобно пользоваться
конвертерами, которые обеспечивают конвертацию
между типами данных модели и представления. К
примеру, чтобы отобразить булево значение в виде
пиктограммы, можно использовать HtmlRenderer, который
умеет отображать HTML-разметку, и конвертер, который
будет превращать булево значение в подходящую
разметку для отображения иконок.
}
@Override
public void init(Map<String, Object> params){
DataGrid.Column hasEmail =
usersGrid.addGeneratedColumn("hasEmail", new
DataGrid.ColumnGenerator<User, Boolean>() {
@Override
public Boolean
getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return
StringUtils.isNotEmpty(event.getItem().getEmail());
}
@Override
public Class<Boolean> getType() {
return Boolean.class;
}
Результат:
}
});
hasEmail.setCaption("Has Email");
hasEmail.setRenderer(usersGrid.createRenderer(DataGrid
.HtmlRenderer.class));
hasEmail.setConverter(new
DataGrid.Converter<String, Boolean>() {
@Override
public Boolean convertToModel(String value,
Class<? extends Boolean> targetType, Locale locale) {
return null;
}
@Override
public String convertToPresentation(Boolean
value, Class<? extends String> targetType, Locale
locale) {
return BooleanUtils.isTrue(value)
?
FontAwesome.CHECK_SQUARE_O.getHtml()
: FontAwesome.SQUARE_O.getHtml();
}
@Override
public Class<Boolean> getModelType() {
return Boolean.class;
}
@Override
public Class<String> getPresentationType() {
return String.class;
}
});
}
Результат:
Создавать рендереры можно двумя способами:
через метод-фабрику интерфейса DataGrid, передавая в
него интерфейс рендерера, для которого нужно создать
имплементацию. Подходит для GUIGUI и WebWeb модулей.
непосредственно создавая имплементацию рендерера
для соответствующего модуля:
dataGrid.createRenderer(DataGrid.ImageRenderer.cla
ss) → new WebImageRenderer()
На данный момент этот способ реализован только для
модуля WebWeb.
Список рендереров, реализованных в платформе:
TextRenderer - рендерер для отображения простого
текста.
HtmlRenderer - рендерер для отображения HTML-
разметки.
ProgressBarRenderer - рендерер, который отображает
double-значения от 0 до 1 в виде компонента
ProgressBar.
DateRenderer - рендерер для отображения дат в
заданном формате.
NumberRenderer - рендерер для отображения чисел в
заданном формате.
ButtonRenderer - рендерер, который использует
строковое значение в качестве заголовка кнопки.
ImageRenderer - рендерер, который использует
строковое значение в качестве пути до изображения.
CheckBoxRenderer - рендерер, который отображает
CheckBoxRenderer - рендерер, который отображает
булево значение в виде иконок чек-бокса.
Header Header и Footer: Footer:
Интерфейсы HeaderRow и FooterRow предназначены для
отображения ячеек заголовков и строк с итогами таблицы
соответственно. Эти ячейки могут быть объединёнными
для нескольких колонок.
Для создания и настройки заголовков и итогов
используются следующие методы:
appendHeaderRow(), appendFooterRow() - добавляет
новую строку внизу секции заголовков/итогов.
prependHeaderRow(), prependFooterRow() - добавляет
новую строку наверху секции заголовков/итогов.
addHeaderRowAt(), addFooterRowAt() - вставляет новую
строку на заданную позицию в секции. Текущая строка
на этой позиции, а также все следующие ниже,
сдвигаются вниз с увеличением их индекса на 1.
removeHeaderRow(), removeFooterRow() - удаляет
указанную строку в секции.
getHeaderRowCount(), getFooterRowCount() -
возвращает количество строк в секции.
setDefaultHeaderRow() - устанавливает заголовок
таблицы по умолчанию. Интерфейс стандартного
заголовка по умолчанию включает в себя элементы для
сортировки колонок таблицы.
Интерфейсы HeaderCell и FooterCell позволяют
управлять статическими ячейками:
setStyleName() - устанавливает пользовательский
стиль для данной ячейки.
getCellType() - возвращает тип содержимого данной
ячейки. Перечисление DataGridStaticCellType
содержит 3 стандартных типа статических ячеек:
TEXT
HTML
COMPONENT
getComponent(), getHtml(), getText() - возвращает
содержимое данной ячейки в зависимости от её типа.
Ниже приведён пример таблицы DataGrid с заголовком,
содержащим объединённые ячейки, и строкой итогов, в
которой отображаются вычисляемые значения:
<dataGrid id="dataGrid"
datasource="countryGrowthDs"
width="100%">
<columns>
<column property="country"/>
<column property="year2014"/>
<column property="year2015"/>
</columns>
</dataGrid>
public class DataGridHeaderFooterFrame extends
AbstractFrame {
@Inject
private DataGrid<CountryGrowth> dataGrid;
@Inject
private CollectionDatasource<CountryGrowth, UUID>
countryGrowthDs;
@Inject
private UserSessionSource userSessionSource;
private DecimalFormat percentFormat;
@Override
public void init(Map<String, Object> params) {
countryGrowthDs.refresh();
countryGrowthDs.refresh();
initPercentFormat();
initHeader();
initFooter();
initRenderers();
}
private DecimalFormat initPercentFormat() {
percentFormat = (DecimalFormat)
NumberFormat.getPercentInstance(userSessionSource.getL
ocale());
percentFormat.setMultiplier(1);
percentFormat.setMaximumFractionDigits(2);
return percentFormat;
}
private void initRenderers() {
dataGrid.getColumnNN("year2014").setRenderer(new
WebNumberRenderer(percentFormat));
dataGrid.getColumnNN("year2015").setRenderer(new
WebNumberRenderer(percentFormat));
}
private void initHeader() {
HeaderRow headerRow =
dataGrid.prependHeaderRow();
HeaderCell headerCell =
headerRow.join("year2014", "year2015");
headerCell.setText("GDP growth");
headerCell.setStyleName("center-bold");
}
private void initFooter() {
FooterRow footerRow =
dataGrid.appendFooterRow();
Атрибуты dataGrid dataGrid
align - caption - colspan - columnResizeMode -
columnsCollapsingAllowed - contextMenuEnabled - datasource -
description - editorBuffered - editorCancelCaption - editorEnabled -
editorSaveCaption - enable - frozenColumnCount - headerVisible -
dataGrid.appendFooterRow();
footerRow.getCell("country").setHtml("<strong>" +
getMessage("average") + "</strong>");
footerRow.getCell("year2014").setText(percentFormat.fo
rmat(getAverage("year2014")));
footerRow.getCell("year2015").setText(percentFormat.fo
rmat(getAverage("year2015")));
}
private double getAverage(String propertyId) {
double average = 0.0;
Collection<CountryGrowth> items =
countryGrowthDs.getItems();
for (CountryGrowth countryGrowth : items) {
Double value =
countryGrowth.getValue(propertyId);
average += value != null ? value : 0.0;
}
return average / items.size();
}
}
ДЕМОНСТРАЦИЯ
editorSaveCaption - enable - frozenColumnCount - headerVisible -
height - icon - id - reorderingAllowed - responsive - rowspan -
selectionMode - settingsEnabled - sortable - stylename - tabIndex -
textSelectionEnabled - visible - width
Элементы dataGrid dataGrid
actions - buttonsPanel - columns - rowsCount
Атрибуты column column
caption - collapsed - collapsible - collapsingToggleCaption - editable
- expandRatio - id - maximumWidth - minimumWidth - property -
resizable - sortable - width
Элементы column column
formatter
APIAPI
addGeneratedColumn - applySettings - createRenderer - editItem -
saveSettings - getColumns - setCellDescriptionProvider -
setCellStyleProvider - setConverter - setEnterPressAction -
setItemClickAction - setRenderer - setRowDescriptionProvider -
setRowStyleProvider - sort
Слушатели dataGrid dataGrid
ColumnCollapsingChangeListener - ColumnReorderListener -
ColumnResizeListener - ContextClickListener - EditorCloseListener -
EditorOpenListener - EditorPostCommitListener -
EditorPreCommitListener - ItemClickListener - SelectionListener -
SortListener
5.5.2.1.10. DateField5.5.2.1.10. DateField
Поле для отображения и ввода даты и
времени. Представляет собой поле даты, внутри которого
имеется кнопка с выпадающим календарем, а правее
находится поле для ввода времени.
XML-имя компонента: dateField.
Компонент DateField реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Для создания поля даты, связанного с данными,
необходимо использовать атрибуты datasource и property:
Как видно из примера, в экране описывается источник
данных orderDs для некоторой сущности Заказ (Order),
имеющей атрибут date. В компоненте ввода даты в
атрибуте datasource указывается ссылка на источник
данных, а в атрибуте property − название атрибута
сущности, значение которого должно быть отображено
в поле.
Если поле связано с атрибутом сущности, то оно
автоматически принимает соответствующий вид:
Если атрибут типа java.sql.Date или указана
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="_local"/>
</dsContext>
<layout>
<dateField datasource="orderDs"
property="date"/>
Если атрибут типа java.sql.Date или указана
аннотация @Temporal(TemporalType.DATE), то поле
времени не отображается. Формат даты определяется
типом данных date и задается в главном пакете
локализованных сообщений в ключе dateFormat.
В противном случае отображается также поле времени
с часами и минутами. Формат времени определяется
типом данных time и задается в главном пакете
локализованных сообщений в ключе timeFormat.
Изменить формат представления даты и времени
можно с помощью атрибута dateFormat. Значением
атрибута может быть либо сама строка формата, либо
ключ в пакете сообщений (если значение начинается с
msg://).
Формат задается по правилам класса SimpleDateFormat
(http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateF
ormat.html). Если в формате отсутствуют символы H или h,
то поле времени не выводится.
DateField в основном предназначен для быстрого
ввода с клавиатуры путем заполнения маски.
Поэтому компонент поддерживает только форматы
с цифрами и разделителями. Сложные форматы с
текстовым представлением дня недели или месяца
не будут работать.
Диапазон доступных дат можно указать с помощью
атрибутов rangeStart и rangeEnd. Если данные
атрибуты установлены, все даты, выходящие за
<dateField dateFormat="MM/yy"
caption="msg://monthOnlyDateField"/>
атрибуты установлены, все даты, выходящие за
пределы диапазона, будут отключены. Значения
доступных даты можно установить в XML в формате
"yyyy-MM-dd", или программно с помощью
соответствующих сеттеров.
Точность представления даты и времени можно
определить с помощью атрибута resolution. Значение
атрибута должно соответствовать перечислению
DateField.Resolution − SEC, MIN, HOUR, DAY, MONTH, YEAR.
По умолчанию - MIN, то есть до минут.
Если resolution="DAY" и не указан атрибут dateFormat,
то в качестве формата будет взят формат, указанный в
главном пакете сообщений с ключом dateFormat.
Если resolution="MIN" и не указан атрибут dateFormat,
то в качестве формата будет взят формат, указанный в
главном пакете сообщений с ключом dateTimeFormat.
Ниже показано определения поля для ввода даты с
точностью до месяца.
<dateField id="dateField" rangeStart="2016-08-15"
rangeEnd="2016-08-19"/>
<dateField resolution="MONTH"
Изменение значения поля DateField, так же, как и
любого другого компонента, реализующего интерфейс
Field, можно отслеживать с помощью слушателя
ValueChangeListener.
Если для пользователя методом setTimeZone() задан
часовой пояс, то DateField может преобразовывать
значения типа timestamp между часовыми поясами
сервера и пользователя. Если компонент привязан к
атрибуту типа timestamp, часовой пояс автоматически
берется из текущей пользовательской сессии. Если нет,
то можно вызвать метод setTimeZone() в контроллере
экрана, чтобы DateField выполнил необходимые
преобразования.
В веб-клиенте с темой, основанной на Halo, к компоненту
DateField можно применить заданный стиль
borderless, чтобы удалить рамку и фон поля. Стили
задаются в XML-дексрипторе или контроллере экрана с
помощью атрибута stylename:
Чтобы применить стиль программно, выберите
константу класса HaloTheme с префиксом компонента
DATEFIELD_:
caption="msg://monthOnlyDateField"/>
<dateField id="dateField"
stylename="borderless"/>
dateField.setStyleName(HaloTheme.DATEFIELD_BORDERLES
S);
ДЕМОНСТРАЦИЯ
Атрибуты dateField dateField
align - caption - datasource - dateFormat - description - editable -
enable - height - icon - id - property - stylename - rangeEnd -
rangeStart - required - requiredMessage - resolution - tabIndex -
visible - width
Элементы dateField dateField
validator
Предопределенные стили dateField dateField
borderless - small - tiny
APIAPI
addValueChangeListener - commit - discard - isModied
5.5.2.1.11. DatePicker5.5.2.1.11. DatePicker
DatePicker это компонент для выбора
даты. Он выглядит так же, как выпадающий календарь в
DateField.
XML-имя компонента: datePicker.
DatePicker реализован для блока Web ClientWeb Client.
Для создания DatePicker, связанного с данными,
необходимо использовать атрибуты datasource и property:
S);
<dsContext>
Как видно из примера, в экране описывается источник
данных orderDs для некоторой сущности Заказ (Order),
имеющей атрибут date. В компоненте ввода даты в
атрибуте datasource указывается ссылка на источник
данных, а в атрибуте property − название атрибута
сущности, значение которого должно быть отображено
в компоненте.
Вы можете указать доступные для выбора даты с
помощью атрибутов rangeStart и rangeEnd. Если вы их
установите, даты, выходящие за пределы указанного
промежутка, будут недоступны.
Точность выбора даты определяется атрибутом
resolution. Значение атрибута должно соответстовать
перечислению DatePicker.Resolution − DAY, MONTH, YEAR.
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="_local"/>
</dsContext>
<layout>
<datePicker id="datePicker" datasource="orderDs"
property="date"/>
<datePicker id="datePicker" rangeStart="2016-08-15"
rangeEnd="2016-08-19"/>
ДЕМОНСТРАЦИЯ
перечислению DatePicker.Resolution − DAY, MONTH, YEAR.
Значение по-умолчанию: DAY.
Атрибуты datePicker datePicker
align - caption - datasource - description - editable - enable - height -
id - property - rangeEnd - rangeStart - resolution - stylename -
tabIndex - visible - width
APIAPI
addValueChangeListener
5.5.2.1.12. Embedded (Deprecated)5.5.2.1.12. Embedded (Deprecated)
Начиная с версии 6.7 платформы, компонент Embedded
объявлен устаревшим (Deprecated). Используйте
компонент Image для отображения графического
содержимого и компонент BrowserFrame для
встраивания веб-страниц.
Компонент Embedded предназначен для
вывода изображений и встраивания в экран
произвольных веб-страниц.
XML-имя компонента: embedded
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient. В десктоп-клиенте поддерживается только вывод
<datePicker id="datePicker" resolution="MONTH"/>
<datePicker id="datePicker" resolution="YEAR"/>
ClientClient. В десктоп-клиенте поддерживается только вывод
изображений.
Рассмотрим пример использования компонента для
вывода изображения из файла, сохраненного в FileStorage.
Объявляем компонент в XML-дескрипторе экрана:
В контроллере экрана инжектируем компонент и
интерфейс FileStorageService. Затем в методе init()
получаем из параметров экрана переданный из
вызывающего кода FileDescriptor, загружаем
соответствующий файл в байтовый массив, создаем для
него ByteArrayInputStream и передаем в метод
setSource() компонента:
<groupBox caption="Embedded" spacing="true"
height="250px" width="250px"
expand="embedded">
<embedded id="embedded" width="100%"
align="MIDDLE_CENTER"/>
</groupBox>
@Inject
private Embedded embedded;
@Inject
private FileStorageService fileStorageService;
@Override
public void init(Map<String, Object> params) {
FileDescriptor imageFile = (FileDescriptor)
params.get("imageFile");
byte[] bytes = null;
if (imageFile != null) {
try {
bytes =
Компонент Embedded может отображать содержимое
различных типов, которые по-разному отрисовываются в
HTML. Тип содержимого можно задать методом setType().
Поддерживаются следующие типы:
OBJECT - позволяет встраивать файлы некоторых типов
в элементы HTML <object> и <embed>.
IMAGE - встраивает изображения в HTML-элемент <img>.
BROWSER - встраивает контейнер для отображения
других независимых документов внутри элемента HTML
<iframe>.
В веб-клиенте компонент позволяет отображать файлы,
находящиеся внутри каталога VAADIN. Например:
или
fileStorageService.loadFile(imageFile);
} catch (FileStorageException e) {
showNotification("Unable to load image
file", NotificationType.HUMANIZED);
}
}
if (bytes != null) {
embedded.setSource(imageFile.getName(), new
ByteArrayInputStream(bytes));
embedded.setType(Embedded.Type.IMAGE);
} else {
embedded.setVisible(false);
}
}
<embedded id="embedded"
relativeSrc="VAADIN/themes/halo/my-
logo.png"/>
Кроме того, можно определить каталог ресурсных файлов
в свойстве приложения cuba.web.resourcesRoot, и указать
для компонента Embedded имя файла внутри этого
каталога с префиксом значения атрибута: file:// , url://
или theme://:
или
Для встраивания в экран веб-клиента внешней веб-
страницы необходимо передать компоненту URL:
Атрибуты embedded embedded
align - height - id - relativeSrc - src - stylename - visible - width
5.5.2.1.13. FieldGroup5.5.2.1.13. FieldGroup
embedded.setRelativeSource("VAADIN/themes/halo/my-
logo.png")
<embedded id="embedded"
src="file://my-logo.png"/>
embedded.setSource("theme://branding/app-icon-
menu.png");
try {
embedded.setSource(new URL("http://www.cuba-
platform.com"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
ДЕМОНСТРАЦИЯ
5.5.2.1.13. FieldGroup5.5.2.1.13. FieldGroup
Компонент FieldGroup предназначен
для совместного отображения и редактирования
нескольких атрибутов сущностей.
XML-имя компонента: fieldGroup
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Пример описания группы полей в XML-дескрипторе
экрана:
Здесь в элементе dsContext определен источник данных
datasource, который содержит один экземпляр сущности
Order. Для компонента fieldGroup в атрибуте datasource
указывается используемый источник данных, а в
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="order-with-customer">
</datasource>
</dsContext>
<layout>
<fieldGroup id="orderFieldGroup"
datasource="orderDs" width="250px">
<field property="date"/>
<field property="customer"/>
<field property="amount"/>
</fieldGroup>
</layout>
элементах field - какие атрибуты сущности,
содержащейся в источнике данных, необходимо
отобразить.
Элементы fieldGroup:
column - необязательный элемент, позволяющий
располагать поля в несколько колонок. Для этого
элементы field должны находиться не
непосредственно внутри fieldGroup, а внутри своего
column. Например:
В данном случае поля будут расположены в две
колонки, причем в первой колонке все поля будут
шириной 250px, а во второй - 400px.
Атрибуты column:
width - задает ширину полей данной колонки. По
умолчанию ширина полей - 200px. В данном атрибуте
ширина может быть задана как в пикселах, так и в
процентах от общего размера колонки по горизонтали.
flex - число, задающее степень изменения общего
размера данной колонки по горизонтали относительно
<fieldGroup id="orderFieldGroup"
datasource="orderDs" width="100%">
<column width="250px">
<field property="num"/>
<field property="date"/>
<field property="amount"/>
</column>
<column width="400px">
<field property="customer"/>
<field property="info"/>
</column>
</fieldGroup>
других колонок при изменении ширины всего
компонента fieldGroup. Например, можно задать одной
колонке flex=1 а другой flex=3.
id - необязательный идентификатор колонки,
позволяющий ссылаться на нее в случае расширении
экрана.
field - основной элемент компонента, описывает одно
поле компонента.
Собственные настраиваемые поля можно определить
внутри элемента field:
Атрибуты элемента field:
id - обязательный атрибут в случае, если не определён
атрибут property; в противном случае по умолчанию
принимает то же значение, что и property. Атрибут id
должен содержать произвольный уникальный
идентификатор либо поля с заданным атрибутом
property, либо программно определяемого поля. В
последнем случае элемент field должен иметь также
атрибут custom="true" (см. далее).
property - обязательный атрибут в случае, если не
определён атрибут id; должен содержать название
атрибута сущности, выводимого в поле, для связывания
поля и данных.
caption − позволяет задать заголовок поля. Если не
задан, будет отображено локализованное название
<fieldGroup>
<field id="demo">
<lookupField id="demoField"
datasource="userDs" property="group"/>
</field>
</fieldGroup>
задан, будет отображено локализованное название
атрибута сущности.
visible − позволяет скрыть поле вместе с заголовком.
datasource − позволяет задать для данного поля
источник данных, отличный от заданного для всего
компонента fieldGroup. Таким образом в группе полей
могут отображаться атрибуты разных сущностей.
optionsDatasource − задает имя источника данных,
используемого для формирования списка опций.
Данный атрибут можно задать для поля, связанного со
ссылочным атрибутом сущности. По умолчанию выбор
связанной сущности производится через экран выбора,
а если optionsDatasource указан, то связанную
сущность можно выбирать из выпадающего списка
опций. Фактически указание optionsDatasource
приводит к тому, что вместо компонента PickerField в
поле используется LookupPickerField.
width − позволяет задать ширину поля без учета
заголовка. По умолчанию ширина поля - 200px. Ширина
может быть задана как в пикселах, так и в процентах от
общего размера колонки по горизонтали. Для указания
ширины всех полей одновременно можно использовать
атрибут width элемента column, описанный выше.
custom - установка этого атрибута в true позволяет
задать собственное представление поля, или говорит о
том, что идентификатор поля не ссылается на атрибут
сущности, и компонент, находящийся в поле, будет
задан программно с помощью метода setComponent()
компонента FieldGroup (см. ниже).
атрибут generator позволяет декларативно создавать
собственные прелставления полей, указав имя метода,
возвращающего компонент для данного поля:
<fieldGroup datasource="productDs">
link - установка атрибута в true позволяет отобразить
вместо поля выбора сущности ссылку на экран
просмотра экземпляра сущности (поддерживается
только для Web ClientWeb Client). Такое поведение может быть
необходимо, если требуется дать пользователю
возможность просматривать связанную сущность, но
саму связь он менять не должен.
linkScreen - позволяет указать идентификатор экрана,
который будет открыт по нажатию на ссылку,
включенную свойством link.
linkScreenOpenType - задает режим открытия экрана
редактирования (THIS_TAB, NEW_TAB или DIALOG).
linkInvoke - позволяет заменить открытие окна на
вызов метода контроллера.
Следующие атрибуты элемента field можно применять
в зависимости от типа атрибута сущности,
отображаемого полем:
<fieldGroup datasource="productDs">
<column width="250px">
<field property="description"
generator="generateDescriptionField"/>
</column>
</fieldGroup>
public Component
generateDescriptionField(Datasource datasource,
String fieldId) {
TextArea textArea =
componentsFactory.createComponent(TextArea.class);
textArea.setRows(5);
textArea.setDatasource(datasource, fieldId);
return textArea;
}
отображаемого полем:
Если для текстового атрибута сущности задать
значение атрибута mask, то в поле вместо компонента
TextField будет использоваться компонент MaskedField с
соотвествующей маской. В этом случае можно также
задать атрибут valueMode.
Если для текстового атрибута сущности задать
значение атрибута rows, то в поле вместо компонента
TextField будет использоваться компонент TextArea с
соответствующим количеством строк. В этом случае
можно также задать атрибут cols.
Для текстового атрибута сущности можно задать
атрибут maxLength аналогично описанному для TextField.
Для атрибута сущности типа date или dateTime можно
задать атрибуты dateFormat и resolution для
параметризации находящегося в поле компонента
DateField.
Для атрибута сущности типа time можно задать
атрибут showSeconds для параметризации
находящегося в поле компонента TimeField.
Атрибуты fieldGroup:
Атрибут border может принимать значение hidden или
visible. По умолчанию - hidden. При установке в
значение visible компонент fieldGroup выделяется
рамкой. В веб-реализации компонента отображение
рамки осуществляется добавлением CSS-класса cuba-
fieldgroup-border.
captionAlignment определяет расположение
заголовков относительно полей компонента
FieldGroup. Принимает два значения: LEFT и TOP.
fieldFactoryBean: декларативные поля, объявленные в
XML-дескрипторе, создаются с помощью интерфейса
FieldGroupFieldFactory. Чтобы переопределить эту
FieldGroupFieldFactory. Чтобы переопределить эту
фабрику, используйте атрибут fieldFactoryBean с
именем вашей реализации FieldGroupFieldFactory.
Для элемента FieldGroup, полностью созданного
программно, для этой цели используется метод
setFieldFactory().
Методы интерфейса FieldGroup:
addField позволяет добавлять поля в FieldGroup на лету.
В качестве параметра принимает экземпляр
FieldConfig, также можно указать положение нового
поля в FieldGroup с помощью параметров colIndex и
rowIndex.
метод bind() необходимо применить к полю после
вызова метода setDatasource(), чтобы вызвать
создание компонентов поля.
createField() используется для создания элементов
FieldGroup, реализующих интерфейс FieldConfig:
Метод getComponent() возвращает визуальный
компонент, находящийся в поле с указанным
идентификатором. Это может потребоваться для
дополнительной параметризации компонента,
недоступной через атрибуты XML-элемента field,
описанные выше.
Вместо явного вызова
getFieldNN("id").getComponentNN() для получения
ссылки на компонент поля в контроллере экрана можно
использовать инжекцию. Для этого следует
использовать аннотацию @Named с указанием
идентификатора самого fieldGroup, и через точку -
fieldGroup.addField(fieldGroup.createField("newField
"));
идентификатора поля.
Например, следующим образом в поле выбора
связанной сущности можно добавить действие
открытия экземпляра и убрать действие очистки поля:
Для использования метода getComponent() или
инжекции компонентов полей необходимо знать тип
компонента, находящегося в поле. В следующей
таблице приведено соответствие типов атрибутов
сущностей и создаваемых для них компонентов:
Тип атрибута сущности Дополнительные условия Тип компонента поля
Связанная сущность
Задан атрибут
optionsDatasource LookupPickerField
PickerField
Перечисление (enum)LookupField
<fieldGroup id="orderFieldGroup"
datasource="orderDs">
<field property="date"/>
<field property="customer"/>
<field property="amount"/>
</fieldGroup>
@Named("orderFieldGroup.customer")
protected PickerField customerField;
@Override
public void init(Map<String, Object> params) {
customerField.addOpenAction();
customerField.removeAction(customerField.getAction(P
ickerField.ClearAction.NAME));
}
Перечисление (enum)
string
Задан атрибут mask MaskedField
Задан атрибут rows TextArea
TextField
boolean CheckBox
date, dateTime DateField
time TimeField
int, long,
double, decimal
Задан атрибут mask MaskedField
TextField
UUID MaskedField с hex-маской
Тип атрибута сущности Дополнительные условия Тип компонента поля
removeField() позволяет удалять поля на лету по id.
Метод setComponent() задаёт собственное
представление поля. Он может быть использован
вместе с атрибутом custom="true" элемента field или с
полем, созданным программно методом createField()
(см. выше). При использовании с custom="true"
необходимо вручную указать источник данных и
свойство.
Экземпляр FieldCong можно получить с помощью
методов getField() или getFieldNN(), и затем вызвать
метод setComponent(), как показано в следующем
примере:
@Inject
protected FieldGroup fieldGroup;
@Inject
protected ComponentsFactory componentsFactory;
@Inject
private Datasource<User> userDs;
Атрибуты fieldGroup fieldGroup
align - border - caption - captionAlignment - datasource - editable -
enable - eldFactoryBean - id - height - stylename - visible - width
Атрибуты column column
ex - id - width
Атрибуты field field
caption - captionProperty - cols - custom - datasource - dateFormat
- description - editable - enable - generator - id - link - linkInvoke -
linkScreen - linkScreenOpenType - mask - maxLength -
optionsDatasource - property - required - requiredMessage -
resolution - rows - showSeconds - tabIndex - visible - width
Элементы field field
column - eld - validator
APIAPI
addField - bind - createField - getComponent - removeField -
setComponent - setFieldFactory
5.5.2.1.14. FileMultiUploadField5.5.2.1.14. FileMultiUploadField
Компонент FileMultiUploadField позволяет
пользователю загружать файлы на сервер. Компонент
представляет собой кнопку, при нажатии на которую на
@Override
public void init(Map<String, Object> params) {
PasswordField passwordField =
componentsFactory.createComponent(PasswordField.clas
s);
passwordField.setDatasource(userDs, "password");
fieldGroup.getFieldNN("password").setComponent(passw
ordField);
}
экране отображается стандартное для операционной
системы окно выбора файлов, в котором можно выбрать
сразу несколько файлов для загрузки.
XML-имя компонента: multiUpload.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Рассмотрим пример использования компонента.
Объявляем компонент в XML-дескрипторе экрана:
В контроллере экрана инжектируем сам компонент, а
также интерфейсы FileUploadingAPI и DataSupplier. Затем в
методе init() добавляем слушателей, которые будут
реагировать на события успешной загрузки или ошибки:
<multiUpload id="multiUploadField"
caption="msg://upload"/>
@Inject
private FileMultiUploadField multiUploadField;
@Inject
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataSupplier dataSupplier;
@Override
public void init(Map<String, Object> params) {
multiUploadField.addQueueUploadCompleteListener(() -
> {
for (Map.Entry<UUID, String> entry :
multiUploadField.getUploadsMap().entrySet()) {
UUID fileId = entry.getKey();
String fileName = entry.getValue();
FileDescriptor fd =
fileUploadingAPI.getFileDescriptor(fileId,
fileName);
// save file to FileStorage
try {
fileUploadingAPI.putFileIntoStorage(fileId, fd);
} catch (FileStorageException e) {
new RuntimeException("Error saving
file to FileStorage", e);
}
// save file descriptor to database
dataSupplier.commit(fd);
}
showNotification("Uploaded files: " +
multiUploadField.getUploadsMap().values(),
NotificationType.HUMANIZED);
multiUploadField.clearUploads();
});
multiUploadField.addFileUploadErrorListener(event ->
showNotification("File upload error",
Компонент загружает выбранные файлы во временное
хранилище клиентского уровня и вызывает слушатель,
добавленный методом
addQueueUploadCompleteListener(). В данном
слушателе вызовом метода getUploadsMap() у
компонента можно получить мэп идентификаторов
файлов во временном хранилище на имена файлов.
Далее для каждого элемента мэп создается
соответствующий объект FileDescriptor путем вызова
FileUploadingAPI.getFileDescriptor(). Объект
com.haulmont.cuba.core.entity.FileDescriptor (не
путать с java.io.FileDescriptor) является
персистентной сущностью, которая однозначно
идентифицирует загруженный файл и впоследствии
используется для выгрузки файла из системы.
Ниже приведён список доступных слушателей для
отслеживания процесса загрузки:
FileUploadStartListener,
FileUploadStartListener,
FileUploadFinishListener,
QueueUploadCompleteListener.
Метод FileUploadingAPI.putFileIntoStorage()
используется для перемещения загружаемого файла из
временного хранилища клиентского уровня в FileStorage.
Параметрами этого метода являются идентификатор
файла во временном хранилище и объект
FileDescriptor.
После загрузки файла в FileStorage выполняется
сохранение экземпляра FileDescriptor в базе данных
посредством вызова DataSupplier.commit().
NotificationType.HUMANIZED));
}
посредством вызова DataSupplier.commit().
Возвращаемый этим методом сохраненный экземпляр
может быть установлен в атрибут какой-либо сущности
предметной области, связанной с данным файлом. В
данном же случае FileDescriptor просто сохраняется в
базе данных. Соответствующий файл будет доступен
через экран AdministrationAdministration → External FilesExternal Files.
После обработки файлов необходимо очистить список
файлов вызовом clearUploads() на случай повторной
загрузки.
Максимальный размер загружаемого файла
определяется свойством приложения
cuba.maxUploadSizeMb и по умолчанию равен 20MB. При
выборе пользователем файла большего размера
выдается соответствующее сообщение и загрузка
прерывается.
XML-атрибут accept (и соответствующий метод
setAccept()) может быть использован для установки
маски расширений файлов в диалоге выбора файла.
Пользователи будут иметь возможность выбрать "All
les" и загрузить произвольные файлы.
Значением атрибута должен быть список масок,
разделенных запятыми. Например: *.jpg,*.png.
XML-атрибут fileSizeLimit (и соответствующий метод
setFileSizeLimit()) может быть использован для
установки максимально допустимого размера файла.
Значением атрибута должно быть целое число для
указания количества байт. Данный лимит
устанавливает ограничение для каждого файла.
XML-атрибут permittedExtensions (и соответствующий
<multiUpload id="multiUploadField"
fileSizeLimit="200000"/>
XML-атрибут permittedExtensions (и соответствующий
метод setPermittedExtensions()) может быть
использован для установки "белого списка" допустимых
расширений загружаемых файлов.
Значением атрибута должен быть набор строк, в
котором каждая строка - это допустимое расширение с
лидирующей точкой. Например:
XML-атрибут dropZone используется для указания
BoxLayout, который будет использован в качестве
целевой площадки для перетаскивания файлов извне
браузера. Если стиль контейнера не переопределён,
выбранный контейнер будет подсвечиваться, когда
пользователь переносит над ним файлы, без наведения
файла контейнер не отображается.
В разделе Загрузка и вывод изображений приведен более
сложный пример работы с загруженными файлами.
Атрибуты multiUpload multiUpload
accept - align - caption - description - dropZone - leSizeLimit -
enable - height - icon - id - permittedExtensions - stylename -
tabIndex - visible - width
Слушатели multiUpload multiUpload
FileUploadErrorListener - FileUploadFinishListener -
FileUploadStartListener - QueueUploadCompleteListener
5.5.2.1.15. FileUploadField5.5.2.1.15. FileUploadField
Компонент FileUploadField позволяет пользователю
загружать файлы на сервер. Компонент может содержать
заголовок, ссылку на загруженный файл и две кнопки: для
uploadField.setPermittedExtensions(Sets.newHashSet("
.png", ".jpg"));
заголовок, ссылку на загруженный файл и две кнопки: для
загрузки файла и для очистки. При нажатии на кнопку
загрузки на экране отображается стандартное для
операционной системы окно, в котором можно выбрать
один файл. Чтобы дать пользователю возможность
загружать сразу несколько файлов, используйте
компонент FileMultiUploadField.
XML-имя компонента: upload.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
FileUploadField автоматически используется внутри
FieldGroup для атрибутов типа FileDescriptor. В этом
случае компонент выглядит как на приведенном выше
скриншоте и не требует никакого конфигурирования.
Загруженный файл сразу же сохраняется в le storage, а
соответствующий FileDescriptor - в базу данных.
Компонент можно использовать вне FieldGroup и
подключить к источнику данных. В примере ниже
подключить к источнику данных. В примере ниже
предполагается, что источник данных personDs
содержит сущность с атрибутом photo, который
является ссылкой на FileDescriptor:
Сохранением файла и FileDescriptor можно также
управлять программно.
Объявляем компонент в XML-дескрипторе экрана:
В контроллере экрана инжектируем сам компонент, а
также интерфейсы FileUploadingAPI и DataSupplier. Затем в
методе init() добавляем слушатели, которые будут
реагировать на события успешной загрузки или ошибки:
<upload fileStoragePutMode="IMMEDIATE"
datasource="personDs"
property="photo"/>
<upload id="uploadField"
fileStoragePutMode="MANUAL"/>
@Inject
private FileUploadField uploadField;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataSupplier dataSupplier;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event -
> {
FileDescriptor fd =
uploadField.getFileDescriptor();
try {
Компонент загружает файл во временное хранилище
клиентского уровня и вызывает слушатель,
добавленный методом
addFileUploadSucceedListener(). В этом слушателе у
компонента запрашивается объект FileDescriptor,
соответствующий загруженному файлу. Объект
com.haulmont.cuba.core.entity.FileDescriptor
является персистентной сущностью, которая
однозначно идентифицирует загруженный файл и
впоследствии используется для выгрузки файла из
системы.
Метод FileUploadingAPI.putFileIntoStorage()
используется для перемещения загружаемого файла из
временного хранилища клиентского уровня в FileStorage.
try {
// save file to FileStorage
fileUploadingAPI.putFileIntoStorage(uploadField.getF
ileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving
file to FileStorage", e);
}
// save file descriptor to database
dataSupplier.commit(fd);
showNotification("Uploaded file: " +
uploadField.getFileName(),
NotificationType.HUMANIZED);
});
uploadField.addFileUploadErrorListener(event ->
showNotification("File upload error",
NotificationType.HUMANIZED));
}
Параметрами этого метода являются идентификатор
файла во временном хранилище и объект
FileDescriptor. Оба эти параметра предоставляет
FileUploadField.
После загрузки файла в FileStorage выполняется
сохранение экземпляра FileDescriptor в базе данных
посредством вызова DataSupplier.commit().
Возвращаемый этим методом сохраненный экземпляр
может быть установлен в атрибут какой-либо сущности
предметной области, связанной с данным файлом. В
данном же случае FileDescriptor просто сохраняется в
базе данных. Соответствующий файл будет доступен
через экран AdministrationAdministration → External FilesExternal Files.
Слушатель, добавленный методом
addFileUploadErrorListener(), вызывается в случае
ошибки загрузки файла во временное хранилище
клиентского уровня.
Ниже приведён полный список доступных слушателей
для отслеживания процесса загрузки:
AfterValueClearListener,
BeforeValueClearListener,
FileUploadErrorListener,
FileUploadFinishListener
FileUploadStartListener,
FileUploadSucceedListener,
ValueChangeListener.
XML-атрибут fileStoragePutMode задает режим
сохранения файла и соответствующего FileDescriptor.
В режиме IMMEDIATE это делается автоматически сразу
после загрузки файла во временное хранилище
клиентского уровня. В режиме MANUAL необходимо
сделать это в листенере FileUploadSucceedListener.
Режим IMMEDIATE выбирается по умолчанию, когда
Режим IMMEDIATE выбирается по умолчанию, когда
FileUploadField используется внутри FieldGroup. В
противном случае, по умолчанию выбирается MANUAL.
XML-атрибуты uploadButtonCaption, uploadButtonIcon и
uploadButtonDescription позволяют задать параметры
кнопки загрузки.
XML-атрибут showFileName управляет отображением
имени загруженного файла рядом с кнопкой загрузки.
По умолчанию false.
XML-атрибут showClearButton управляет видимостью
кнопки очистки. По умолчанию false.
XML-атрибуты clearButtonCaption, clearButtonIcon и
clearButtonDescription позволяют задать параметры
кнопки очистки, если она видима.
XML-атрибут accept (и соответствующий метод
setAccept()) может быть использован для установки
маски расширений файлов в диалоге выбора файла.
Пользователи будут иметь возможность выбрать "All
les" и загрузить произвольные файлы.
Значением атрибута должен быть список масок,
разделенных запятыми. Например: *.jpg,*.png.
Максимальный размер загружаемого файла
определяется свойством приложения
cuba.maxUploadSizeMb и по умолчанию равен 20MB. При
выборе пользователем файла большего размера
выдается соответствующее сообщение и загрузка
прерывается.
XML-атрибут fileSizeLimit (и соответствующий метод
setFileSizeLimit()) может быть использован для
установки максимально допустимого размера файла.
Значением атрибута должно быть целое число для
указания количества байт.
XML-атрибут permittedExtensions (и соответствующий
метод setPermittedExtensions()) может быть
использован для установки "белого списка" допустимых
расширений загружаемых файлов.
Значением атрибута должен быть набор расширений с
лидирующими точками, разделенных запятыми.
Например:
XML-атрибут dropZone используется для указания
BoxLayout, который будет использован в качестве
целевой площадки для перетаскивания файлов извне
браузера. Зона перетаскивания может занимать всю
площадь диалогового окна. Выбранный контейнер
будет подсвечиваться, когда пользователь переносит
над ним файлы, без наведения файла контейнер не
отображается.
<upload id="uploadField" fileSizeLimit="2000"/>
uploadField.setPermittedExtensions(Sets.newHashSet("
.png", ".jpg"));
<layout spacing="true"
width="100%">
<vbox id="dropZone"
height="AUTO"
spacing="true">
<textField id="textField"
caption="Title"
width="100%"/>
<textArea id="textArea"
caption="Description"
width="100%"
Чтобы сделать область dropZone статической и
отображать её постоянно, необходимо назначить её
контейнеру готовый стиль dropzone-container. В этом
случае контейнер необходимо оставить пустым,
поместив в него только текстовый компонент label:
width="100%"
rows="5"/>
<checkBox caption="Is reference document"
width="100%"/>
<upload id="upload"
dropZone="dropZone"
showClearButton="true"
showFileName="true"/>
</vbox>
<hbox spacing="true">
<button caption="mainMsg://actions.Apply"/>
<button caption="mainMsg://actions.Cancel"/>
</hbox>
</layout>
<layout spacing="true"
width="100%">
width="100%">
<textField id="textField"
caption="Title"
width="100%"/>
<checkBox caption="Is reference document"
width="100%"/>
<upload id="upload"
dropZone="dropZone"
showClearButton="true"
showFileName="true"/>
<vbox id="dropZone"
height="150px"
spacing="true"
stylename="dropzone-container">
<label stylename="dropzone-description"
value="Drop file here"
align="MIDDLE_CENTER"/>
</vbox>
<hbox spacing="true">
<button caption="mainMsg://actions.Apply"/>
<button caption="mainMsg://actions.Cancel"/>
</hbox>
</layout>
ДЕМОНСТРАЦИЯ
В разделе Загрузка и вывод изображений приведен более
сложный пример работы с загруженными файлами.
Атрибуты upload upload
accept - align - caption - clearButtonCaption -
clearButtonDescription - clearButtonIcon - datasource - description -
dropZone - editable - enable - leSizeLimit - leStoragePutMode -
height - icon - id - permittedExtensions - property - showClearButton
- showFileName - stylename - tabIndex - uploadButtonCaption -
uploadButtonDescription - uploadButtonIcon - visible - width
APIAPI
addValueChangeListener - commit - discard - isModied
Слушатели upload upload
AfterValueClearListener - BeforeValueClearListener -
FileUploadErrorListener - FileUploadFinishListener -
FileUploadStartListener - FileUploadSucceedListener
5.5.2.1.16. Filter5.5.2.1.16. Filter
Компонент Filter − универсальное
средство фильтрации списков сущностей, извлекаемых из
базы данных для отображения в табличном виде.
Компонент позволяет производить быструю фильтрацию
данных по произвольному набору условий, а также
создавать фильтры для многократного использования.
Filter должен быть связан с источником данных
collectionDatasource содержащим запрос на JPQL.
Принцип действия фильтра основан на модификации
этого запроса в соответствии с критериями, заданными
пользователем. Таким образом фильтрация
осуществляется на уровне БД при выполнении
транслированного из JPQL в SQL запроса, и на Middleware и
клиентский уровень загружаются только отобранные
данные.
Использование фильтра
Типичный фильтр выглядит следующим образом:
По умолчанию, компонент находится в режиме быстрой
фильтрации. Это означает, что пользователь может
добавить набор условий для однократного поиска
данных. После закрытия экрана просмотра экземпляров
сущности условия будут удалены.
Для того чтобы создать быстрый фильтр, нажмите на
ссылку Add search condition (Add search condition (Добавить условие поиска)).
Отобразится экран выбора условий:
Рассмотрим возможные типы условий:
Properties (Properties (Атрибуты)) – атрибуты данной сущности и
связанных с ней сущностей. Отображаются
персистентные атрибуты, явно заданные в элементе
property XML-описателя фильтра, либо
соответствующие правилам, указанным в элементе
properties.
Custom conditions (Custom conditions (Специальные условия)) – условия,
заданные разработчиком в элементах custom XML-
описателя фильтра.
Create new (Create new (Создать новое)) – позволяет создать новое
произвольное условие на JPQL. Данный пункт доступен
пользователю, если у него есть специфическое
разрешение cuba.gui.filter.customConditions.
Выбранные условия отображаются в верхней части
панель фильтра. Рядом с каждым условием находится
кнопка , позволяющая удалить их из набора.
Быстрый фильтр можно сохранить для повторного
использования в дальнейшем. Для этого нажмите на
кнопку настроек фильтра и выберите Save/Save asSave/Save as
((Сохранить//Сохранить как)). Во всплывающем окне
задайте имя нового фильтра:
Фильтр будет сохранен в выпадающем меню кнопки
Search (Search (Поиск)).
Пункт меню Reset lter (Reset lter (Сбросить фильтр)) позволяет
Пункт меню Reset lter (Reset lter (Сбросить фильтр)) позволяет
сбросить все текущие условия поиска.
Кнопка настроек фильтра содержит выпадающий список
опций для управления фильтром:
Save (Save (Сохранить)) – сохранить изменения в текущем
фильтре.
Save with values (Save with values (Сохранить со значениями)) – сохранить
изменения в текущем фильтре, использовав значения в
редакторах параметров фильра как дефолтные
значения фильтра.
Save as (Save as (Сохранить как)) – сохранить фильтр под новым
именем.
Edit (Edit (Редактировать)) – открыть редактор фильтра (см.
ниже).
Make default (Make default (Установить по умолчанию)) – установить
фильтр по умолчанию для данного экрана. Фильтр
будет автоматически выводиться на панель при каждом
открытии экрана.
Remove (Remove (Удалить)) – удалить текущий фильтр.
Pin applied (Pin applied (Закрепить)) – использовать результаты
последнего поиска для последовательной фильтрации
данных (см. Последовательное наложение фильтров).
Save as search folder (Save as search folder (Сохранить как папку поиска)) –
создать папку поиска на основе текущего фильтра.
Save as application folder (Save as application folder (Сохранить как папку
приложения)) – создать папку приложения на основе
текущего фильтра. Эта опция доступна только
пользователям со специфическим разрешением
cuba.gui.appFolder.global.
Опция EditEdit открывает редактор фильтра, который дает
возможность расширенной настройки текущего фильтра:
Название фильтра указывается в поле Filter name (Filter name (Имя
фильтра)). Это имя будет отображаться в списке
доступных фильтров для текущего экрана.
Фильтр можно сделать
global
(то есть доступным для всех
пользователей) с помощью установки флажка Available forAvailable for
all users (all users (Общий)) для всех пользователей, или установить
текущий фильтр в качестве фильтра по умолчанию с
помощью установки флажка Default (Default (По умолчанию)). Для
этой операции пользователю требуется специфическое
разрешение CUBA > Фильтр > Создание/изменение
глобальных фильтров.
В дереве содержатся условия фильтра. Условия можно
добавлять с помощью кнопки Add (Add (Добавить)) менять
местами при помощи кнопок / или удалять с
помощью кнопки Remove (Remove (Удалить)).
Группировку условий по И или ИЛИ можно добавить с
помощью соответствующих кнопок. Все добавленные на
верхний уровень (то есть без явной группировки) условия
объединяются по И.
При выборе условия в дереве в правой части редактора
При выборе условия в дереве в правой части редактора
открывается список его свойств.
С помощью соответствующих флажков можно сделать
выбранное в таблице условие скрытым или обязательным
для заполнения. Параметр скрытого условия не
отображается пользователю, поэтому он должны быть
введен во время редактирования фильтра.
Свойство WidthWidth позволяет задать ширину поля ввода
параметра для текущего условия. По умолчанию, условия
на панели фильтров отображаются в три колонки.
Ширина поля равняется количеству колонок, которое оно
может занять (1, 2 или 3).
Значение параметра текущего условия по умолчанию
можно задать в поле Default value (Default value (Значение по
умолчанию)).
Специальный заголовок условия фильтрации можно
задать в поле Caption (Caption (Заголовок)).
Поле OperationOperation позволяет выбрать оператор поиска. Список
доступных операторов зависит от типа атрибута.
Описание компонента Filter Filter
XML-имя компонента: filter.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Пример объявления компонента в XML-дескрипторе
экрана:
<dsContext>
<collectionDatasource id="carsDs"
class="com.company.sample.entity.Car"
view="carBrowse">
<query>
select c from ref$Car c order by
Здесь в элементе dsContext определен источник данных
collectionDatasource, который выбирает экземпляры
сущности Car с помощью JPQL запроса. Для компонента
filter в его атрибуте datasource указан фильтруемый
источник данных. Данные отображаются компонентом
Table, связанным с этим же источником.
Элемент filter может содержать вложенные элементы.
Все они описывают условия, доступные пользователю для
выбора в диалоге добавления условий:
properties - позволяет сделать доступными сразу
несколько атрибутов сущности. Данный элемент может
иметь следующие атрибуты:
include - обязательный атрибут, содержит регулярное
выражение, которому должно соответствовать имя
атрибута сущности.
exclude - содержит регулярное выражение, при
select c from ref$Car c order by
c.createTs
</query>
</collectionDatasource>
</dsContext>
<layout>
<filter id="carsFilter" datasource="carsDs">
<properties include=".*"/>
</filter>
<table id="carsTable" width="100%">
<columns>
<column id="vin"/>
<column id="model.name"/>
<column id="colour.name"/>
</columns>
<rows datasource="carsDs"/>
</table>
</layout>
exclude - содержит регулярное выражение, при
соответствии которому атрибут сущности исключается
из ранее включенных с помощью include.
Например:
При использовании элемента properties
автоматически игнорируются следующие атрибуты
сущности:
Недоступные в связи с разрешениями подсистемы
безопасности.
Коллекции (@OneToMany, @ManyToMany).
Неперсистентные атрибуты.
Атрибуты, не имеющие локализованного названия.
Атрибуты, аннотированные @SystemLevel.
Атрибуты типа byte[].
Атрибут version.
property - явно включает атрибут сущности по имени.
Данный элемент может иметь следующие атрибуты:
name - обязательный атрибут, содержит имя
включаемого атрибута сущности. Может быть путем
(через ".") по графу сущностей. Например:
<filter id="transactionsFilter"
datasource="transactionsDs">
<properties include=".*"
exclude="(masterTransaction)|(authCode)"/>
</filter>
<filter id="transactionsFilter"
datasource="transactionDs" applyTo="table">
<properties include=".*"
exclude="(masterTransaction)|(authCode)"/>
<property name="creditCard.maskedPan"
caption - локализованное название атрибута сущности
для отображения условия фильтра. Как правило,
представляет из себя строку с префиксом msg:// по
правилам MessageTools.loadString().
Если в атрибуте name указан путь (через ".") по графу
сущностей, то атрибут caption является обязательным.
paramWhere − задает выражение на JPQL для отбора
списка значений параметра условия, если параметр
является связанной сущностью. Вместо алиаса
сущности параметра в выражении нужно использовать
метку (placeholder) {E}.
Например, предположим, что сущность Car имеет
ссылку на сущность Model. Тогда список возможных
значений параметра может быть ограничен только
моделями Audi:
В выражении JPQL можно использовать параметры
экрана, атрибуты сессии, а также компоненты экрана, в
том числе отображающие другие параметры. Правила
задания параметров запроса описаны в Запросы в
CollectionDatasourceImpl.
Пример использования параметра сессии и параметра
экрана:
<property name="creditCard.maskedPan"
caption="msg://EmbeddedCreditCard.maskedPan"/>
<property name="creditCard.startDate"
caption="msg://EmbeddedCreditCard.startDate"/>
</filter>
<filter id="carsFilter" datasource="carsDs">
<property name="model"
paramWhere="{E}.manufacturer = 'Audi'"/>
</filter>
экрана:
Используя paramWhere можно вводить зависимости
между параметрами. Например, предположим, что
Manufacturer является отдельной сущностью. То есть
Car ссылается на Model, которая в свою очередь
ссылается на Manufacturer. Тогда для фильтра по Car
можно создать два условия: первое для выбора
Manufacturer и второе для выбора Model. Чтобы
ограничить список моделей выбранным перед этим
производителем, добавьте в выражение paramWhere
параметр:
Здесь параметр ссылается на компонент,
отображающий параметр Manufacturer. Имя компонента,
отображающего параметр условия, можно узнать,
вызвав контекстное меню на строке таблицы условий в
редакторе фильтра:
{E}.createdBy = :session$userLogin and {E}.name like
:param$groupName
{E}.manufacturer.id =
:component$filter.model_manufacturer90062
paramView − задает представление, с которым будет
загружаться список значений параметра условия, если
параметр является связанной сущностью. Например,
_local. Если не указано, используется _minimal.
custom - элемент, определяющий произвольное условие.
Содержимым элемента должно быть выражение на JPQL
(возможно использование JPQL Macros), которое будет
добавлено в условие where запроса источника данных.
Вместо алиаса отбираемой сущности в выражении
нужно использовать метку (placeholder) {E}. Параметр
условия может быть только один, и если он есть,
обозначается символом ?.
Значение условия может содержать спецсимволы,
например "%" или "_" для оператора "like". Если вам нужно
экранировать эти символы, добавьте в условие escape
'<char>', например:
Тогда если в значении параметра условия будет
передано foo\%, поиск будет интерпретировать "%" как
символ в имени а не как спецсимвол.
Пример фильтра с произвольными условиями:
{E}.name like ? escape '\'
<filter id="carsFilter" datasource="carsDs">
<properties include=".*"/>
<custom name="vin" paramClass="java.lang.String"
caption="msg://vin">
{E}.vin like ?
</custom>
<custom name="colour"
Созданные custom условия отображаются в секции
Специальные условия диалога добавления условий:
Атрибуты элемента custom:
name − обязательный атрибут - имя условия.
caption − обязательный атрибут - локализованное
название условия. Как правило, представляет из себя
строку с префиксом msg:// по правилам
MessageTools.loadString().
paramClass="com.company.sample.entity.Colour"
caption="msg://colour"
inExpr="true">
({E}.colour.id in (?))
</custom>
<custom name="repair"
paramClass="java.lang.String" caption="msg://repair"
join="join {E}.repairs cr">
cr.description like ?
</custom>
<custom name="updateTs"
caption="msg://updateTs">
@between({E}.updateTs, now-1, now+1, day)
</custom>
</filter>
MessageTools.loadString().
paramClass − Java-класс параметра условия. Если
параметр отсутствует, то данный атрибут не
обязателен.
inExpr − должен быть установлен в true, если
выражение JPQL содержит условие in (?). При этом
пользователь будет иметь возможность ввести
несколько значений параметра данного условия.
join − необязательный атрибут для задания строки,
которая будет добавлена в секцию from запроса
источника данных. Это может потребоваться для
создания условия по атрибуту связанной коллекции.
Значение данного атрибута должно включать в себя
предложения join или left join.
Например, предположим что сущность Car имеет
атрибут repairs, который представляет собой
коллекцию экземпляров связанной сущности Repair.
Тогда для фильтрации Car по атрибуту description
сущности Repair можно написать следующее условие:
При использовании такого условия исходный запрос
источника данных:
<filter id="carsFilter" datasource="carsDs">
<custom name="repair"
caption="msg://repair"
paramClass="java.lang.String"
join="join {E}.repairs cr">
cr.description like ?
</custom>
</filter>
select c from sample$Car c order by c.createTs
будет трансформирован в следующий:
paramWhere − задает выражение на JPQL для отбора
списка значений параметра условия, если параметр
является связанной сущностью. См. описание
одноименного атрибута элемента property.
paramView − задает представление, с которым будет
загружаться список значений параметра условия, если
параметр является связанной сущностью. См. описание
одноименного атрибута элемента property.
Атрибуты filter:
editable - если значение этого атрибута равно false, то
кнопка Фильтр скрывается.
manualApplyRequired − определяет, в какой момент
будет применяться фильтр. Если значение атрибута
равно false, то фильтр (пустой или по умолчанию)
будет применяться сразу при открытии экрана. Это
означает, что источник данных будет обновлен и
связанные компоненты (например, Table) отобразят
данные. Если значение атрибута равно true, то фильтр
будет применяться только после нажатия на кнопку
SearchSearch.
Данный атрибут имеет приоритет над свойством
приложения cuba.gui.genericFilterManualApplyRequired.
useMaxResults − ограничивает размер страницы
загружаемых в источник данных экземпляров
сущности. По умолчанию true.
Если значение этого атрибута равно false, то фильтр не
select c from sample$Car c join c.repairs cr
where (cr.description like ?)
order by c.createTs
Если значение этого атрибута равно false, то фильтр не
будет отображать поле Show rowsShow rows. Количество записей в
источнике данных (и соответственно, показываемых
таблицей) будет ограничено только параметром
MaxFetchUI механизма статистики сущностей, по
умолчанию - 10000.
Если данный атрибут не указан, или равен true, то поле
Show rowsShow rows отображается, если у пользователя также
есть специфическое разрешение cuba.gui.lter.maxResults.
Если разрешение cuba.gui.filter.maxResults
отсутствует, то фильтр будет принудительно отбирать
только первые N строк без возможности пользователя
отключить это или указать другое N. Число N
определяется параметрами FetchUI, DefaultFetchUI,
получаемыми из механизма статистики сущностей.
На рисунке далее показан вид фильтра со значением
атрибута useMaxResults="true", запретом
специфического разрешения
cuba.gui.filter.maxResults и параметром
DefaultFetchUI=2
textMaxResults - позволяет использовать текстовое
поле вместо выпадающего списка в качестве поля ShowShow
rowsrows. По умолчанию false.
folderActionsEnabled − при указании значения false
позволяет скрыть следующие действия с фильтром:
Сохранить как папку поиска, Сохранить как папку
приложения. По умолчанию значение атрибута равно
приложения. По умолчанию значение атрибута равно
true, действия Сохранить как папку поиска,
Сохранить как папку приложения доступны.
applyTo − необязательный атрибут, содержит
идентификатор компонента, с которым связан фильтр.
Используется в случае, когда необходимо иметь доступ
к представлениям связанного компонента-таблицы.
Например, сохраняя фильтр как папку поиска или как
папку приложения, можно указать, какое
представление будет применятся при просмотре этой
папки.
caption - позволяет задать заголовок панели фильтров.
columnsCount - задает количество колонок с условиями
для конкретного фильтра. Значение по умолчанию - 3.
defaultMode - задает режим фильтра при открытии
экрана. Возможные значения: generic и fts. При
указании значения fts фильтр будет открыт в режиме
полнотекстового поиска (если сущность
индексируется). Значение по умолчанию - generic.
modeSwitchVisible - определяет видимость чек-бокса
modeSwitchVisible - определяет видимость чек-бокса
для перевода фильтра в режим полнотекстового
поиска. Если полнотекстовый поиск невозможен, то чек-
бокс будет невидим независимо от указаного значения.
Возможные значения атрибута: true и false.
Методы интерфейса Filter: Filter:
setBorderVisible() - определяет видимость границы
фильтра. Значение по умолчанию - true.
Слушатели компонента Filter: Filter:
ExpandedStateChangeListener - позволяет отслеживать
изменения состояния компонета
(свёрнутое/развёрнутое).
FilterEntityChangeListener - срабатывает при первом
выборе фильтра и дальнейшем выборе сохранённых
фильтров.
Атрибуты filter filter
applyTo - caption - columnsCount - datasource - defaultMode -
editable - enable - folderActionsEnabled - id - manualApplyRequired
- margin - modeSwitchVisible - settingsEnabled - stylename -
textMaxResults - useMaxResults - visible - width
Элементы filter filter
custom - properties - property
Атрибуты properties properties
exclude - include
Атрибуты property property
caption - name - paramView - paramWhere
Атрибуты custom custom
caption - name - inExpr - join - paramClass - paramView -
paramWhere
APIAPI
addExpandedStateChangeListener - addFilterEntityChangeListener -
addExpandedStateChangeListener - addFilterEntityChangeListener -
applySettings - getMargin - saveSettings - setMargin
Права пользователей
Для создания/изменения/удаления глобальных
(доступных всем пользователям) фильтров
пользователь должен иметь разрешение
cuba.gui.filter.global.
Для создания/изменения custom условий пользователь
должен иметь разрешение
cuba.gui.filter.customConditions.
Чтобы иметь возможность изменять максимальное
количество строк на странице таблицы с помощью
флажка и поля Show rowsShow rows пользователь должен иметь
разрешение cuba.gui.filter.maxResults. См. также
атрибут фильтра useMaxResults.
Информация о том, как настраивать специфические
разрешения, приведена в руководстве Подсистема
безопасности.
Внешние параметры для управления фильтрами
Общесистемные параметры
Следующие свойства приложения влияют на поведение
фильтров:
cuba.gui.genericFilterManualApplyRequired − позволяет
отключить автоматическое применение фильтра (то
есть загрузку данных) сразу при открытии экрана. См.
также атрибут фильтра manualApplyRequired.
cuba.gui.genericFilterChecking − позволяет включить
проверку заполненности хотя-бы одного условия перед
применением фильтра.
cuba.gui.genericFilterControlsLayout − определяет
расположение элементов внутри фильтра.
cuba.allowQueryFromSelected позволяет отключить
механизм последовательного наложения фильтров.
cuba.gui.genericFilterColumnsCount - задает количество
колонок по умолчанию для размещения условий
фильтра. См также атрибут фильтра columnsCount.
cuba.gui.genericFilterConditionsLocation - задает
расположение панели условий.
cuba.gui.genericFilterPopupListSize - задает количество
позиций в выпадающем списке кнопки SearchSearch.
Параметры вызова экрана
При вызове экрана можно указать, какой фильтр и с
какими параметрами должен быть применен сразу
после открытия экрана. Для этого фильтр должен быть
заранее создан, сохранен в базе данных, и
соответствующая запись в таблице SEC_FILTER должна
иметь заполненное поле CODE. Параметры вызова
экрана задаются в конфигурационном файле web-
menu.xml.
Чтобы сохранить фильтр в базе данных, необходимо
добавить скрипт вставки фильтра в скрипт 30.create-
db.sql сущности. Для генерации скрипта найдите
фильтр в справочнике Entity InspectorEntity Inspector меню AdministrationAdministration, в
контекстном меню выберите System InformationSystem Information, нажмите
на кнопку Script for insertScript for insert и скопируйте текст скрипта.
Теперь можно добавить скрипт к экрану. Для указания
кода фильтра в экран следует передать параметр с
именем, равным идентификатору компонента фильтра
в данном экране. Значением параметра должен быть
код фильтра, который нужно установить и применить.
Для установки значений параметров фильтра в экран
нужно передать параметры с именами, равными
именам параметров, и значения в виде строк.
Пример описателя пункта главного меню,
Пример описателя пункта главного меню,
устанавливающего в открываемом экране
sample$Car.browse в компоненте carsFilter фильтр с
кодом FilterByVIN, с подстановкой в параметр условия
component$carsFilter.vin79216 значения TMA:
Следует отметить, что фильтр с установленным полем
CODE обладает особыми свойствами:
Его не могут редактировать пользователи.
Название такого фильтра можно отображать на
нескольких языках. Для этого в главном пакете
сообщений приложения должна быть строка с ключом,
равным коду фильтра.
Последовательное наложение фильтров
При включенном свойстве приложения
cuba.allowQueryFromSelected в пользовательском
интерфейсе компонента можно закреплять последний
примененный фильтр и текущие результаты фильтрации.
После этого можно выбрать другой фильтр или
параметры и применить их на уже выбранных записях.
Данный подход позволяет решить две проблемы:
Декомпозировать сложные фильтры.
Применять фильтры на записи, отобранные с помощью
папок приложения или поиска.
Чтобы применить этот механизм в пользовательском
интерфейсе, выберите и примените один из фильтров.
Затем нажмите на кнопку настроек фильтра и выберите
<item id="sample$Car.browse">
<param name="carsFilter" value="FilterByVIN"/>
<param name="component$carsFilter.vin79216"
value="TMA"/>
</item>
Затем нажмите на кнопку настроек фильтра и выберите
Pin applied (Pin applied (Закрепить)). Фильтр закрепится в верхней
части панели фильтров. Далее можно применить к
выбранным записям другой фильтр. Так последовательно
можно накладывать друг на друга любое количество
фильтров. Также фильтры можно удалять
последовательно с помощью кнопки .
Механизм последовательного наложения фильтров
основан на возможности DataManager выполнять
последовательные запросы.
API API для работы с параметрами фильтра
Интерфейс Filter предоставляет методы для установки
и чтения значений параметра фильтра из кода
контроллера экрана:
setParamValue(String paramName, Object value)
getParamValue(String paramName)
paramName - имя параметра фильтра. Имя параметра
фильтра является составной частью имени компонента,
отображающего значение параметра фильтра. Как
получить имя компонента рассматривалось выше. Имя
параметра - это часть имени компонента, находящаяся
после последней точки. Например, если имя компонента
component$filter.model_manufacturer90062, то имя
параметра фильтра model_manufacturer90062.
Обратите внимание, что в методе init() контроллера экрана
данные методы использовать нельзя, т.к. в этот момент
фильтр еще не проинициализирован. Вы можете работать
с параметрами фильтра в методе ready().
Режим полнотекстового поиска в фильтре
Если источник данных фильтра содержит сущности,
индексируемые системой полнотекстового поиска (см.
Платформа CUBA. Полнотекстовый поиск), то в фильтре
становится доступным режим полнотекстового поиска.
Чтобы переключиться в него, используйте флажок Full-TextFull-Text
SearchSearch ("Полнотекстовый поиск").
В этом режиме фильтр имеет всего одно поле для задание
критериев поиска, и поиск производится по
индексируемым подсистемой FTS полям сущности.
Если таблица указана в атрибуте applyTo, то при
наведении указателя мыши на строку таблицы во
ДЕМОНСТРАЦИЯ
всплывающем окне будет написано, в каких полях
сущности было найдено условие поиска.
Для скрытия переключателя режима фильтра установите
значение false атрибуту фильтра modeSwitchVisible.
Если необходимо, чтобы фильтр по умолчанию
открывался в режиме полнотекстового поиска,
установите значение fts атрибуту defaultMode.
Полнотекстовый поиск может использоваться совместно с
любым количеством условий универсального фильтра:
Выбрать условие FTS conditionFTS condition можно в окне выбора
условий фильтра.
5.5.2.1.17. GroupTable5.5.2.1.17. GroupTable
Компонент GroupTable - это таблица с
возможностью динамической группировки по любому
полю. Для того чтобы сгруппировать таблицу по какой-
либо колонке, нужно в заголовке таблицы перетащить эту
колонку в позицию слева от элемента .
Сгруппированные значения можно разворачивать и
сворачивать с помощью кнопок / .
XML-имя компонента: groupTable.
Компонент реализован только для блока Web ClientWeb Client. В
Desktop ClientDesktop Client ведет себя как обычная таблица.
Для GroupTable в атрибуте datasource элемента rows
должен быть указан groupDatasource. В противном случае
группировка работать не будет.
Пример использования:
<dsContext>
<groupDatasource id="ordersDs"
class="com.sample.sales.entity.Order"
view="orderWithCustomer">
<query>
select o from sales$Order o order by
o.date
</query>
</groupDatasource>
</dsContext>
<layout>
<groupTable id="ordersTable" width="100%">
<columns>
<group>
<column id="date"/>
</group>
<column id="customer.name"/>
<column id="amount"/>
</columns>
group − необязательный элемент, может в единственном
экземпляре находиться внутри columns. Содержит набор
элементов column, по которым будет выполняться
первоначальная группировка при открытии экрана.
Элемент column может содержать атрибут groupAllowed с
булевым значением. С помощью этого атрибута можно
запретить пользователю группировать таблицу по данной
колонке.
При включенном атрибуте aggregatable таблица
отображает результаты агрегации по всем строкам в
дополнительной строке вверху, а также результаты
агрегации по группам. Отображение агрегации по всем
строкам можно отключить, установив false в атрибуте
showTotalAggregation.
При включенном атрибуте multiselect клик по строке
группировки с зажатой клавишей CtrlCtrl разворачивает
группу (если была свёрнута) и применяет выделение ко
всем строкам этой группы. При этом, если вся группа
выделена, Ctrl+clickCtrl+click не снимает выделение со всей группы.
Вы по-прежнему можете снять выделение с отдельных
строк, пользуясь стандартным поведением клавиши CtrlCtrl.
В остальном функциональность GroupTable аналогична
простой таблице Table.
Атрибуты groupTable groupTable
align - aggregatable - aggregationStyle - columnControlVisible -
contextMenuEnabled - editable - enable - height - id - multiLineCells
- multiselect - presentations - reorderingAllowed - settingsEnabled -
showTotalAggregation - sortable - stylename - tabIndex -
textSelectionEnabled - visible - width
</columns>
<rows datasource="ordersDs"/>
</groupTable>
ДЕМОНСТРАЦИЯ
textSelectionEnabled - visible - width
Элементы groupTable groupTable
actions - buttonsPanel - columns - rows - rowsCount
Элементы columns columns
column - group
Атрибуты columncolumn
align - caption - captionProperty - collapsed - dateFormat - editable -
groupAllowed - id - link - linkInvoke - linkScreen -
linkScreenOpenType - maxTextLength - optionsDatasource -
resolution - sortable - visible - width
Элементы columncolumn
aggregation - formatter
Атрибуты rowsrows
datasource
APIAPI
addColumnCollapseListener - setClickListener
5.5.2.1.18. Image5.5.2.1.18. Image
Компонент Image предназначен для
отображения графического содержимого из различных
источников. Компонент можно привязать к источнику
данных или сконфигурировать программно.
XML имя компонента: image.
Компонент Image может отображать значение атрибута
сущности с типом FileDescriptor или byte[]. Для этого
используются атрибуты datasource и property, например:
В данном случае компонент отображает атрибут avatar
<image id="image" datasource="employeeDs"
property="avatar"/>
В данном случае компонент отображает атрибут avatar
сущности Employee, находящейся в источнике данных
employeeDs.
Помимо источников данных, компонент Image может
использовать в качестве источника содержимого
различные типы ресурсов. Тип ресурса можно указать
декларативно с помощью элементов image,
перечисленных ниже:
classpath - ресурс, расположенный в classpath.
file - файл с изображением.
relativePath - относительный путь к файлу в каталоге
приложения.
theme - ресурс из темы приложения, например:
VAADIN/themes/customTheme/some/path/image.png.
<image>
<classpath
path="com/company/sample/web/screens/myPic.jpg"/>
</image>
<image>
<file
path="D:\sample\modules\web\web\VAADIN\images\myImag
e.jpg"/>
</image>
<image>
<relativePath path="VAADIN/images/myImage.jpg"/>
</image>
url - ресурс, загружаемый по URL.
Атрибуты image:
scaleMode - устанавливает режим масштабирования
изображения. Доступны следующие режимы:
FILL - изображение масштабируется, чтобы заполнить
всю область компонента: используется вся ширина и
высота компонента.
CONTAIN - изображение подстраивается под размер
компонента с сохранением пропорций, уменьшаясь или
растягиваясь по меньшей стороне компонента.
SCALE_DOWN - изображение изменяет размер, сравнивая
разницу между NONE и CONTAIN, для того, чтобы найти
наименьший конкретный размер объекта.
NONE - изображение сохранит свой исходный размер,
размер компонента будет равен размеру изображения.
alternateText - устанавливает альтернативный текст
на случай, если ресурс недоступен или не задан.
Параметры ресурсов image:
<image>
<theme path="com.company.sample/myPic.jpg"/>
</image>
<image>
<url url="https://www.cuba-
platform.com/sites/all/themes/cuba_adaptive/img/lori
.png"/>
</image>
<image id="image" alternateText="logo"/>
Параметры ресурсов image:
bufferSize - размер буфера, используемого для
загрузки этого ресурса, в байтах.
cacheTime - время хранения объекта в кэше в
миллисекундах.
mimeType - MIME-тип ресурса.
Методы интерфейса Image:
setDatasource() - устанавливает для изображения
источник данных и его атрибут. Поддерживаются
только атрибуты типов FileDescriptor и byte[].
Программное указание источника данных позволяет, к
примеру, отображать изображения в ячейках таблицы:
<image>
<file bufferSize="1024" path="C:/img.png"/>
</image>
<image>
<file cacheTime="2400" path="C:/img.png"/>
</image>
<image>
<url
url="https://avatars3.githubusercontent.com/u/175485
14?v=4&s=200"
mimeType="image/png"/>
</image>
frameworksTable.addGeneratedColumn("image", entity -
> {
setSource() - устанавливает источник изображения.
Метод принимает тип ресурса и возвращает объект
ресурса, который может быть сконфигурирован далее.
Для каждого типа ресурсов есть свои методы, например,
setPath() для ThemeResource или setStreamSupplier()
для StreamResource:
или
> {
Image image =
componentsFactory.createComponent(Image.class);
image.setDatasource(frameworksTable.getItemDatasourc
e(entity), "image");
image.setHeight("100px");
return image;
});
Image image =
componentsFactory.createComponent(Image.class);
image.setSource(ThemeResource.class)
.setPath("images/image.png");
image.setSource(StreamResource.class)
.setStreamSupplier(() -> new
FileDataProvider(fileDescriptor).provide())
.setBufferSize(1024);
Можно использовать следующие типы ресурсов,
реализующие интерфейс Resource, или расширить их:
ClasspathResource - для изображений, хранимых в
classpath. Этот ресурс также можно использовать
декларативно с помощью элемента classpath
компонента image.
FileDescriptorResource - для изображений,
получаемых из FileStorage.
FileResource - для изображений, хранимых в файловой
системе. Этот ресурс также можно использовать
декларативно с помощью элемента file компонента
image.
RelativePathResource - для изображений, хранимых в
каталоге приложения. Этот ресурс также можно
использовать декларативно с помощью элемента
relativePath компонента image.
StreamResource - для изображений, получаемых из
потока.
ThemeResource - для изображений темы, например,
VAADIN/themes/yourtheme/some/path/image.png. Этот
ресурс также можно использовать декларативно с
помощью элемента theme компонента image.
UrlResource - для изображений, загружаемых по
указанному URL. Этот ресурс также можно
использовать декларативно с помощью элемента url
компонента image.
createResource() - создаёт ресурс изображения
указанного типа. Созданный объект может быть позже
передан в метод setSource().
FileDescriptorResource resource =
image.createResource(FileDescriptorResource.class)
ДЕМОНСТРАЦИЯ
addSourceChangeListener() - добавляет слушатель для
отслеживания изменений источника изображения.
Атрибуты image image
align - alternateText - caption - colspan - datasource - description -
editable - enable - height - icon - id - property - required -
requiredMessage - responsive - rowspan - scaleMode - stylename -
stylename - visible - width
Атрибуты ресурсов image image
bufferSize - cacheTime - mimeType
Элементы image image
classpath - le - relativePath - theme - url
APIAPI
addSourceChangeListener - createResource - setScaleMode -
setDatasource - setSource
5.5.2.1.19. Label5.5.2.1.19. Label
Надпись (Label) − текстовый
компонент, отображающий статический текст либо
значение атрибута сущности.
XML-имя компонента: label
Компонент Label реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Пример задания надписи с текстом, взятым из пакета
локализованных сообщений:
image.createResource(FileDescriptorResource.class)
.setFileDescriptor(avatar);
image.setSource(resource);
<label value="msg://orders"/>
Атрибут value предназначен для задания текста надписи.
В веб клиенте текст, содержащийся в атрибуте value,
будет разбит на несколько строк, если по длине он
превысит значение атрибута width. Поэтому для
отображения многострочной надписи, достаточно указать
абсолютное значение атрибута width. Если текст надписи
слишком длинный, а значение атрибута width не
определено, то текст будет урезан.
Параметры надписи можно задать в контроллере экрана.
Для этого необходимо задать компоненту идентификатор,
по которому получить ссылку на него в контроллере:
Компонент Label может отображать значение атрибута
сущности. Для этого используются атрибуты datasource и
property. Например:
<label value="Label, which should be split into
multiple lines"
width="200px"/>
<label id="dynamicLabel"/>
@Inject
private Label dynamicLabel;
public void init(Map<String, Object> params) {
dynamicLabel.setValue("Some value");
}
<dsContext>
В данном случае компонент отображает атрибут name
сущности Customer, находящейся в источнике данных
customerDs.
Атрибут htmlEnabled указывает, каким образом будет
рассматриваться значение атрибута value: при
htmlEnabled="true" как HTML-код, иначе как строка.
Обратите внимание, что не все HTML-теги
поддерживаются в десктоп-реализации экрана.
Стили компонента Label Label
В веб-клиенте с темой, основанной на Halo, к компоненту
Label можно применить заданные стили. Стили
задаются в XML-дексрипторе или контроллере экрана с
помощью атрибута stylename:
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
LABEL_:
bold - жирный шрифт. Подходит для выделения
важных текстовых элементов UI.
colored - цветной текст.
<datasource id="customerDs"
class="com.sample.sales.entity.Customer"
view="_local"/>
</dsContext>
<layout>
<label datasource="customerDs" property="name"/>
<label value="Label to be styled"
stylename="colored"/>
label.setStyleName(HaloTheme.LABEL_COLORED);
colored - цветной текст.
failure - стиль сообщения об ошибке. Добавляет
рамку вокруг компонента и пиктограмму рядом с
текстом. Используется как уведомление рядом с
другим компонентом.
h1 - стиль основных заголовков приложения.
h2 - стиль заголовков разделов приложения.
h3 - стиль подзаголовков.
h4 - стиль подзаголовков.
light - облегченный шрифт. Подходит для выделения
второстепенных текстовых элементов UI.
no-margin - убирает отступы заголовков.
spinner - стиль спиннера. Используйте для пустых
компонентов Label, чтобы создать спиннер.
success - стиль сообщения об успешном выполнении.
Добавляет рамку вокруг компонента и пиктограмму
рядом с текстом. Используется как уведомление
рядом с другим компонентом.
Атрибуты label label
align - datasource - enable - height - htmlEnabled - icon - id -
property - stylename - value - visible - width
Элементы label label
formatter
Предопределенные стили label label
bold - colored - failure - h1 - h2 - h3 - h4 - huge - large - light - no-
margin - small - spinner - success - tiny
APIAPI
addValueChangeListener
5.5.2.1.20. Link5.5.2.1.20. Link
ДЕМОНСТРАЦИЯ
ДЕМОНСТРАЦИЯ
5.5.2.1.20. Link5.5.2.1.20. Link
Ссылка (Link) − компонент-
гиперссылка, позволяющая открывать внешние веб-
ресурсы единообразно для веб и десктоп клиента.
XML-имя компонента: link
Пример XML-описания компонента link:
Атрибуты link:
url - адрес ресурса.
target - для веб клиента задает способ открытия
страницы, аналогичен атрибуту target HTML-тега <a>.
Атрибуты link link
align - caption - description - enable - icon - id - stylename - url -
target - visible - width
5.5.2.1.21. LinkButton5.5.2.1.21. LinkButton
Кнопка-ссылка (LinkButton) − кнопка,
выглядящая как гиперссылка.
XML-имя компонента: linkButton
Компонент кнопки-ссылки реализован для блоков WebWeb
ClientClient и Desktop ClientDesktop Client.
Кнопка-ссылка может содержать текст или пиктограмму
(или и то и другое). На рисунке ниже отражены разные
виды кнопок.
<link caption="Link" url="https://www.cuba-
platform.com" target="_blank"/>
ДЕМОНСТРАЦИЯ
Кнопка-ссылка отличается от обычной кнопки Button
только своим внешним видом. Все свойства и поведение
идентичны описанным для Button.
Пример XML-описания кнопки-ссылки, вызывающей метод
someMethod() контроллера, с надписью (атрибут caption),
всплывающей подсказкой (атрибут description) и
пиктограммой (атрибут icon):
Атрибуты linkButton linkButton
action - align - caption - description - enable - icon - id - invoke -
stylename - visible - width
5.5.2.1.22. LookupField5.5.2.1.22. LookupField
Компонент для выбора значения из
выпадающего списка. Выпадающий список реализует
фильтрацию значений по мере ввода пользователя и
постраничный вывод доступных значений.
XML-имя компонента: lookupField.
<linkButton id="linkButton"
caption="msg://linkButton"
description="Press me"
icon="icons/save.png"
invoke="someMethod"/>
Компонент LookupField реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Простейший вариант использования LookupField -
выбор значения перечисления (enum) для атрибута
сущности. Например, сущность Role имеет атрибут type
типа RoleType, который является перечислением. Тогда
для редактирования этого атрибута можно
использовать LookupField следующим образом:
Как видно из примера, в экране описывается источник
данных roleDs для сущности Role. В компоненте
lookupField в атрибуте datasource указывается ссылка
на источник данных, а в атрибуте property − название
атрибута сущности, значение которого должно быть
отображено. В данном случае атрибут является
перечислением, и в выпадающем списке будут
отображены локализованные названия всех значений
этого перечисления.
Аналогично можно использовать LookupField для
выбора экземпляра связанной сущности. Для
формирования списка опций используется атрибут
optionsDatasource:
<dsContext>
<datasource id="roleDs"
class="com.haulmont.cuba.security.entity.Role"
view="_local"/>
</dsContext>
<layout>
<lookupField datasource="roleDs"
property="type"/>
<dsContext>
<datasource id="carDs"
В данном случае компонент отобразит отобразит имена
экземпляров сущности Colour, находящихся в
источнике данных colorsDs, а выбранное значение
подставится в атрибут colour сущности Car,
находящейся в источнике данных carDs.
С помощью атрибута captionProperty можно указать,
какой атрибут сущности использовать вместо имени
экземпляра для строковых названий опций.
Список опций компонента может быть задан
произвольно с помощью методов setOptionsList(),
setOptionsMap() и setOptionsEnum(), либо с помощью
XML-атрибута optionsDatasource.
Метод setOptionsList() позволяет программно задать
список опций компонента. Для этого объявляем
компонент в XML-дескрипторе:
Затем инжектируем компонент в контроллер и в методе
init() задаем ему список опций:
<datasource id="carDs"
class="com.company.sample.entity.Car"
view="_local"/>
<collectionDatasource id="coloursDs"
class="com.company.sample.entity.Colour"
view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<lookupField datasource="carDs"
property="colour" optionsDatasource="coloursDs"/>
<lookupField id="numberOfSeatsField"
datasource="modelDs" property="numberOfSeats"/>
init() задаем ему список опций:
В выпадающем списке компонента отобразятся числа 2,
4, 5, 7. Выбранное число подставится в атрибут
numberOfSeats сущности, находящейся в источнике
данных modelDs.
Метод setOptionsMap() позволяет задать строковые
названия и значения опций по отдельности. Например,
для описанного в XML-дескрипторе компонента
numberOfSeatsField в методе init() контроллера
задаем мэп опций:
@Inject
protected LookupField numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(4);
list.add(5);
list.add(7);
numberOfSeatsField.setOptionsList(list);
}
@Inject
protected LookupField numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("two", 2);
map.put("four", 4);
map.put("five", 5);
map.put("seven", 7);
В выпадающем списке компонента отобразятся строки
two, four, five, seven. Однако значением компонента
будет число, соответствующее выбранной строке. Оно и
подставится в атрибут numberOfSeats сущности,
находящейся в источнике данных modelDs.
setOptionsEnum() принимает в качестве параметра
класс перечисления. Выпадающий список будет
содержать локализованные названия значений
перечисления, значением компонента будет являться
выбранное значение перечисления.
Каждый элемент выпадающего списка может иметь
пиктограмму слева. Создайте реализацию интерфейса
LookupField.OptionIconProvider в контроллере экрана
и установите ее для компонента lookupField:
numberOfSeatsField.setOptionsMap(map);
}
lookupField.setOptionIconProvider(new
LookupField.OptionIconProvider<Customer>(){
@Override
public String getItemIcon(Customer c){
if(c.getType()== LegalStatus.LEGAL)
return"icons/icon-office.png";
return"icons/icon-user.png";
}
});
С помощью атрибута filterMode можно задать тип
фильтрации опций при вводе пользователя:
NO − нет фильтрации.
STARTS_WITH − по началу фразы.
CONTAINS − по любому вхождению (используется по
умолчанию).
Если у компонента LookupField не установлен атрибут
required, и если связанный атрибут сущности не объявлен
как обязательный, то в списке опций компонента
присутствует пустая строка, при выборе которой
компонент возвращает значение null. Атрибут nullName
позволяет задать строку, отображаемую в этом случае
вместо пустой. Пример использования:
В данном случае вместо пустой строки отобразится
строка (none), при выборе которой в связанный атрибут
сущности подставится значение null.
При программном задании списка опций методом
setOptionsList() можно одну из опций передать в
метод setNullOption(). Тогда при ее выборе
пользователем значением компонента будет null.
Компонент LookupField способен обрабатывать ввод
пользователя при отсутствии подходящей опции в
списке. Для этого используются методы
setNewOptionAllowed() и setNewOptionHandler().
Например:
<lookupField datasource="carDs" property="colour"
optionsDatasource="coloursDs" nullName="(none)"/>
@Inject
Обработчик NewOptionHandler вызывается, если
пользователь ввел некоторое значение, не
совпадающее ни с одной из опций, и нажал EnterEnter. В
данном случае в обработчике создается новый
экземпляр сущности Colour, его атрибут name
устанавливается в значение, введенное пользователем,
этот экземпляр добавляется в источник данных опций и
выбирается в компоненте.
Вместо имплементации интерфейса
LookupField.NewOptionHandler для обработки ввода
пользователя можно использовать XML-атрибут
newOptionHandler с указанным в нем методом
контроллера. Данный метод должен иметь два
параметра - первый типа LookupField, второй типа
protected LookupField colourField;
@Inject
protected CollectionDatasource<Colour, UUID>
coloursDs;
@Override
public void init(Map<String, Object> params) {
colourField.setNewOptionAllowed(true);
colourField.setNewOptionHandler(new
LookupField.NewOptionHandler() {
@Override
public void addNewOption(String caption) {
Colour colour = new Colour();
colour.setName(caption);
coloursDs.addItem(colour);
colourField.setValue(colour);
}
});
}
параметра - первый типа LookupField, второй типа
String. В них будут переданы соответственно
экзампляр компонента и введенное пользователем
значение. Атрибут newOptionAllowed используется
вместо метода setNewOptionAllowed() для того, чтобы
разрешить добавление новых опций.
XML-атрибут nullOptionVisible устанавливает
видимость элемента со значением null в списке опций.
Может работать только если атрибут required имеет
значение false.
XML-атрибут textInputAllowed используется для
отключения возможности фильтрации опций с
клавиатуры. Это бывает удобно для коротких списков.
Значение по умолчанию - true.
XML-атрибут pageLength позволяет переопределить
количество опций на одной странице выпадающего
списка, заданное свойством приложения
cuba.gui.lookupFieldPageLength.
В веб-клиенте с темой, основанной на Halo, к компоненту
LookupField можно применить предопределенные
стили. Стили задаются в XML-дексрипторе или
контроллере экрана с помощью атрибута stylename:
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
LOOKUPFIELD_:
<lookupField id="lookupField"
stylename="borderless"/>
lookupField.setStyleName(HaloTheme.LOOKUPFIELD_BORDE
RLESS);
ДЕМОНСТРАЦИЯ
Стили компонента LookupField:
align-center - align the text inside the eld to center.
align-right - align the text inside the eld to the right.
borderless - removes the border and background from the text
eld.
Атрибуты lookupField lookupField
align - caption - captionProperty - datasource - description - editable
- enable - lterMode - height - icon - id - inputPrompt -
newOptionAllowed - newOptionHandler - nullName -
nullOptionVisible - optionsDatasource - pageLength - property -
required - requiredMessage - stylename - tabIndex -
textInputAllowed - visible - width
Элементы lookupField lookupField
validator
Предопределенные стили lookupField lookupField
align-right - align-center - borderless - huge - large - small - tiny
APIAPI
addValueChangeListener - commit - discard - isModied -
setOptionsEnum - setOptionsList - setOptionsMap
5.5.2.1.23. LookupPickerField5.5.2.1.23. LookupPickerField
Компонент LookupPickerField
позволяет отображать экземпляр сущности в текстовом
поле, выбирать экземпляр в выпадающем списке и
выполнять действия нажатием на кнопки справа.
XML-имя компонента: lookupPickerField.
XML-имя компонента: lookupPickerField.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
LookupPickerField является по сути гибридом LookupField
и PickerField, поэтому все описанное для этих интерфейсов
верно и для него. Исключением является список действий
по умолчанию, добавляемых при определении
компонента в XML: для LookupPickerField это действия
lookup и open .
Пример использования LookupPickerField для выбора
значения ссылочного атрибута colour сущности Car:
Атрибуты lookupPickerField lookupPickerField
align - caption - captionProperty - datasource - description - editable
- enable - lterMode - height - icon - id - inputPrompt - metaClass -
newOptionAllowed - newOptionHandler - nullName -
optionsDatasource - pageLength - property - required -
requiredMessage - stylename - tabIndex - visible - width
Элементы lookupPickerField lookupPickerField
actions - validator
<dsContext>
<datasource id="carDs"
class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs"
class="com.company.sample.entity.Colour"
view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<lookupPickerField datasource="carDs"
property="colour" optionsDatasource="coloursDs"/>
ДЕМОНСТРАЦИЯ
actions - validator
Предопределённые стили lookupPickerField lookupPickerField
huge - large
APIAPI
addValueChangeListener - commit - discard - isModied
5.5.2.1.24. MaskedField5.5.2.1.24. MaskedField
Текстовое поле, в которое данные
вводятся в определенном формате. MaskedField удобно
использовать, например, для ввода телефонных номеров.
XML-имя компонента: maskedField.
Компонент MaskedField реализован только для блока WebWeb
ClientClient.
MaskedField в основном повторяет функциональность
TextField, за исключением того, что ему нельзя установить
datatype. То есть MaskedField предназначен для работы
только с текстом и строковыми атрибутами сущностей.
MaskedField имеет следующие специфические атрибуты:
mask - задает маску для поля. Чтобы задать маску,
используются следующие символы:
# - цифра
U - буква верхнего регистра
L - буква нижнего регистра
? - буква
А - буква или цифра
* - любой символ
H - hex символ в верхнем регистре
h - hex символ в нижнем регистре
~ - знак + или -
valueMode - определяет формат возвращаемого
значения (с маской, или без) и может принимать
значения (с маской, или без) и может принимать
значение masked или clear.
Пример текстового поля с маской для ввода номеров
телефонов:
Атрибуты maskedField maskedField
align - caption - datasource - description - editable - enable - height -
icon - id - mask - maxLength - property - required - requiredMessage
- stylename - tabIndex - trim - valueMode - visible - width
Элементы maskedField maskedField
validator
APIAPI
addEnterPressListener - addValueChangeListener
<maskedField id="phoneNumberField" mask="(###)###-##-
##" valueMode="masked"/>
<button caption="msg://showPhoneNumberBtn"
invoke="showPhoneNumber"/>
@Inject
private MaskedField phoneNumberField;
public void showPhoneNumber(){
showNotification((String)
phoneNumberField.getValue(),
NotificationType.HUMANIZED);
}
ДЕМОНСТРАЦИЯ
5.5.2.1.25. OptionsGroup5.5.2.1.25. OptionsGroup
Компонент, который обеспечивает
выбор из списка опций, используя группу переключателей
для выбора единственного значения или группу флажков
для выбора нескольких значений.
XML-имя компонента: optionsGroup.
Компонент OptionsGroup реализован для блоков Web ClientWeb Client
и Desktop ClientDesktop Client.
Простейший вариант использования OptionsGroup -
выбор значения перечисления (enum) для атрибута
сущности. Например, сущность Role имеет атрибут type
типа RoleType, который является перечислением. Тогда
для редактирования этого атрибута можно
использовать OptionsGroup следующим образом:
Как видно из примера, в экране описывается источник
данных roleDs для сущности Role. В компоненте
optionsGroup в атрибуте datasource указывается ссылка
<dsContext>
<datasource id="roleDs"
class="com.haulmont.cuba.security.entity.Role"
view="_local"/>
</dsContext>
<layout>
<optionsGroup datasource="roleDs"
property="type"/>
optionsGroup в атрибуте datasource указывается ссылка
на источник данных, а в атрибуте property − название
атрибута сущности, значение которого должно быть
отображено.
В результате компонент примет следующий вид:
Список опций компонента может быть задан
произвольно с помощью методов setOptionsList(),
setOptionsMap() и setOptionsEnum(), либо с помощью
XML-атрибута optionsDatasource.
Метод setOptionsList() позволяет программно задать
список опций компонента. Для этого объявляем
компонент в XML-дескрипторе:
Затем инжектируем компонент в контроллер и в методе
init() задаем ему список опций:
<optionsGroup id="numberOfSeatsField"/>
@Inject
protected OptionsGroup numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(4);
list.add(5);
list.add(7);
numberOfSeatsField.setOptionsList(list);
}
Компонент примет следующий вид:
При этом метод getValue() компонента в зависимости
от выбранной опции будет возвращать Integer
значения 2,4,5,7.
Метод setOptionsMap() позволяет задать строковые
названия и значения опций по отдельности. Например,
для описанного в XML-дескрипторе компонента
numberOfSeatsField в методе init() контроллера
задаем мэп опций:
Компонент примет следующий вид:
При этом метод getValue() компонента в зависимости
}
@Inject
protected OptionsGroup numberOfSeatsField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("two", 2);
map.put("four", 4);
map.put("five", 5);
map.put("seven", 7);
numberOfSeatsField.setOptionsMap(map);
}
При этом метод getValue() компонента в зависимости
от выбранной опции будет возвращать Integer
значения 2,4,5,7, а не строки, отображаемые на экране.
setOptionsEnum() принимает в качестве параметра
класс перечисления. Список опций будет состоять из
локализованных названий значений перечисления,
значением компонента будет являться выбранное
значение перечисления.
Компонент может брать список опций из источника
данных. Для этого используется атрибут
optionsDatasource. Например:
В данном случае компонент coloursField отобразит
имена экземпляров сущности Colour, находящихся в
источнике данных coloursDs, а его метод getValue()
вернет выбранный экземпляр сущности.
С помощью атрибута captionProperty можно указать,
какой атрибут сущности использовать вместо имени
экземпляра для строковых названий опций.
С помощью атрибута multiselect можно переключить
OptionsGroup в режим множественного выбора. Если
multiselect включен, то компонент отображается как
группа независимых флажков, а значением компонента
<dsContext>
<collectionDatasource id="coloursDs"
class="com.company.sample.entity.Colour"
view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<optionsGroup id="coloursField"
optionsDatasource="coloursDs"/>
группа независимых флажков, а значением компонента
является список выбранных опций.
Например, создадим в XML-дескрипторе экрана
компонент:
И в контроллере зададим для него список опций -
значения перечисления RoleType:
Компонент примет следующий вид:
В данном случае метод getValue() компонента вернет
объект типа java.util.List, содержащий значения
RoleType.READONLY и RoleType.DENYING.
Этот пример иллюстрирует также способность
компонента OptionsGroup автоматически отображать
локализованные значения перечислений, входящих в
модель данных приложения.
Атрибут orientation задает расположение элементов
<optionsGroup id="roleTypesField"
multiselect="true"/>
@Inject
protected OptionsGroup roleTypesField;
@Override
public void init(Map<String, Object> params) {
roleTypesField.setOptionsList(Arrays.asList(RoleType
.values()));
}
Атрибут orientation задает расположение элементов
группы. По умолчанию элементы располагаются по
вертикали. Значение horizontal задает
горизонтальное расположение.
Атрибуты optionsGroup optionsGroup
align - caption - captionProperty - datasource - description - editable
- enable - icon - id - multiselect - height - optionsDatasource -
orientation - property - required - requiredMessage - stylename -
tabIndex - visible - width
Элементы optionsGroup optionsGroup
validator
APIAPI
addValueChangeListener - setOptionsEnum - setOptionsList -
setOptionsMap
5.5.2.1.26. OptionsList5.5.2.1.26. OptionsList
OptionsList представляет собой вариацию компонента
OptionsGroup с представлением опций в виде
вертикального прокручиваемого списка. Если включена
возможность множественного выбора, элементы могут
быть выбраны с удерживанием клвиши Ctrl при клике или
диапазона при удерживании клавиши Shift.
XML-имя компонента: optionsList.
Компонент OptionsList реализован для блока Web ClientWeb Client.
ДЕМОНСТРАЦИЯ
Компонент OptionsList реализован для блока Web ClientWeb Client.
По умолчанию компонент OptionsList отображает
первый пустой элемент в списке опций. Пустой элемент
можно скрыть с помощью атрибута nullOptionVisible,
установив ему значение false.
Единственная разница в API между OptionsList и
OptionsGroup заключается в том, что OptionsList не имеет
атрибута orientation.
Атрибуты optionsList optionsList
align - caption - captionProperty - datasource - description - editable
- enable - height - icon - id - multiselect - nullOptionVisible -
optionsDatasource - property - required - requiredMessage -
stylename - tabIndex - visible - width
Элементы optionsList optionsList
validator
APIAPI
addValueChangeListener
5.5.2.1.27. PasswordField5.5.2.1.27. PasswordField
Текстовое поле, которое вместо
символов, введенных пользователем, отображает эхо-
символы.
XML-имя компонента: passwordField.
PasswordField реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
PasswordField в основном аналогичен компоненту
TextField, за исключением того, что ему нельзя установить
datatype. То есть PasswordField предназначен для
работы только с текстом и строковыми атрибутами
сущностей.
ДЕМОНСТРАЦИЯ
Пример использования:
Атрибут autocomplete позволяет включить сохранение
паролей в веб браузере. По умолчанию отключено.
Атрибуты passwordField passwordField
align - autocomplete - caption - datasource - description - editable -
enable - height - icon - id - maxLength - property - required -
requiredMessage - stylename - tabIndex - visible - width
Элементы passwordField passwordField
validator
APIAPI
addValueChangeListener
5.5.2.1.28. PickerField5.5.2.1.28. PickerField
PickerField позволяет отображать
<passwordField id="passwordField"
caption="msg://name"/>
<button caption="msg://buttonsName"
invoke="showPassword"/>
@Inject
private PasswordField passwordField;
public void showPassword(){
showNotification((String)
passwordField.getValue(), NotificationType.HUMANIZED);
}
PickerField позволяет отображать
экземпляр сущности в текстовом поле и выполнять
действия нажатием на кнопки справа.
XML-имя компонента: pickerField.
Компонент PickerField реализован для блоков Web ClientWeb Client
и Desktop ClientDesktop Client.
Как правило, PickerField используется для работы со
ссылочными атрибутами сущностей. При этом
компоненту достаточно указать атрибуты datasource и
property:
Как видно из примера, в экране описывается источник
данных carDs для некоторой сущности Car, имеющей
атрибут colour. В элементе pickerField в атрибуте
datasource указывается ссылка на источник данных, а в
атрибуте property − название атрибута сущности,
значение которого должно быть отображено в
компоненте. Атрибут сущности должен являться
ссылкой на другую сущность, в приведенном примере
это Colour.
Для PickerField можно определить произвольное
количество действий, отображаемых кнопками справа.
Это можно сделать как в XML-дескрипторе с помощью
<dsContext>
<datasource id="carDs"
class="com.company.sample.entity.Car"
view="_local"/>
</dsContext>
<layout>
<pickerField datasource="carDs"
property="colour"/>
Это можно сделать как в XML-дескрипторе с помощью
вложенного элемента actions, так и программно в
контроллере методом addAction().
Существуют стандартные действия, определенные
перечислением PickerField.ActionType: lookup, clear,
open. Они выполняют соответственно выбор связанной
сущности, очистку поля и открытие экрана
редактирования выбранной связанной сущности. Для
стандартных действий в XML не нужно определять
никаких атрибутов, кроме идентификатора. Если при
объявлении компонента никаких действий в элементе
actions не задано, загрузчик XML определит для него
действия lookup и clear. Чтобы добавить к действиям
по умолчанию, например, действие open, нужно
определить элемент actions следующим образом:
Элемент action не дополняет, а переопределяет набор
стандартных действий, поэтому необходимо указывать
идентификаторы всех требуемых действий. Компонент
примет следующий вид:
Для программного задания стандартных действий
служат методы addLookupAction(), addOpenAction() и
addClearAction(). Если компонент определен в XML-
дескрипторе без вложенного элемента actions, то
<pickerField datasource="carDs" property="colour">
<actions>
<action id="lookup"/>
<action id="open"/>
<action id="clear"/>
</actions>
</pickerField>
достаточно добавить недостающие действия:
Если же компонент создается в контроллере, то
никаких действий по умолчанию он не получает, и
необходимо добавить все нужные действия явно:
Стандартные действия можно параметризовать. В XML-
дескрипторе возможности для этого ограничены:
существует только атрибут openType, в котором можно
задать режим открытия экрана выбора (для
LookupAction) или редактирования (для OpenAction).
При программном создании действий можно задать
любые свойства объектов PickerField.LookupAction,
@Inject
protected PickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.addOpenAction();
}
@Inject
protected ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
PickerField colourField =
componentsFactory.createComponent(PickerField.NAME);
colourField.setDatasource(carDs, "colour");
colourField.addLookupAction();
colourField.addOpenAction();
colourField.addClearAction();
}
PickerField.OpenAction и PickerField.ClearAction,
возвращаемых методами добавления стандартных
действий. Например, так можно задать специфический
экран выбора:
Подробнее см. JavaDocs классов стандартных действий.
Произвольные действия в XML-дескрипторе также
определяются во вложенном элементе actions,
например:
Программно задать произвольное действие можно
следующим образом:
PickerField.LookupAction lookupAction =
customerField.addLookupAction();
lookupAction.setLookupScreen("customerLookupScreen"
);
<pickerField datasource="carDs" property="colour">
<actions>
<action id="lookup"/>
<action id="show" icon="icons/show.png"
invoke="showColour" caption=""/>
</actions>
</pickerField>
@Inject
protected PickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.addAction(new
AbstractAction("show") {
@Override
Декларативное и программное создание действий
подробно описано в разделе Действия. Интерфейс
Action.
Компонент PickerField можно использовать без
непосредственной привязки к данным, то есть без
указания datasource и property. В этом случае для
указания типа сущности, с которой должен работать
PickerField, используется атрибут metaClass.
Например:
Экземпляр выбранной сущности можно получить,
инжектировав компонент в контроллер и вызвав его
метод getValue().
@Override
public void actionPerform(Component
component) {
showColour(colourField.getValue());
}
@Override
public String getCaption() {
return "";
}
@Override
public String getIcon() {
return "icons/show.png";
}
});
}
<pickerField id="colourField"
metaClass="sample$Colour"/>
ДЕМОНСТРАЦИЯ
Для правильной работы компонента PickerField
необходима либо установка атрибута metaClass,
либо одновременная установка атрибутов
datasource и property.
В компоненте PickerField можно использовать горячие
клавиши: см. Горячие клавиши.
Атрибуты pickerField pickerField
align - caption - captionProperty - datasource - description - editable
- enable - height - icon - id - metaClass - property - required -
requiredMessage - stylename - tabIndex - visible - width
Элементы pickerField pickerField
actions - validator
APIAPI
addAction - addClearAction - addLookupAction - addOpenAction -
addValueChangeListener - commit - discard - isModied -
setLookupScreen
5.5.2.1.29. PopupButton5.5.2.1.29. PopupButton
Кнопка с выпадающим меню. Меню
может содержать список действий или отображать
собственное содержимое.
XML-имя компонента: popupButton.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Кнопка PopupButton может содержать текст, заданный с
помощью атрибута caption, или пиктограмму (или и то, и
другое). Всплывающую подсказку можно задать с
другое). Всплывающую подсказку можно задать с
помощью атрибута description. На рисунке ниже отражены
разные виды кнопок:
Элементы popupButton:
actions - определяет выпадающий список действий.
Отображаются только следующие свойства действий:
caption, enable, visible. Свойства description,
shortcut игнорируются. Обработка свойства icon
зависит от свойства приложения
cuba.gui.showIconsForPopupMenuActions и от атрибута
showActionIcons компонента. Последний имеет
приоритет.
Пример кнопки с выпадающим списком, содержащим
два действия:
popup - позволяет создать собственное содержимое
всплывающего меню. Если оно задано, элемент actions
игнорируется.
<popupButton id="popupButton"
caption="msg://popupButton" description="Press me">
<actions>
<action id="popupAction1"
caption="msg://action1" invoke="someAction1"/>
<action id="popupAction2"
caption="msg://action2" invoke="someAction2"/>
</actions>
</popupButton>
игнорируется.
Пример кнопки с собственным содержимым:
<popupButton id="popupButton"
caption="Settings"
align="MIDDLE_CENTER"
icon="font-icon:GEARS"
closePopupOnOutsideClick="true"
popupOpenDirection="BOTTOM_CENTER">
<popup>
<vbox width="250px"
height="AUTO"
spacing="true"
margin="true">
<label value="Settings"
align="MIDDLE_CENTER"
stylename="h2"/>
<progressBar caption="Progress"
width="100%"/>
<textField caption="New title"
width="100%"/>
<lookupField caption="Status"
optionsEnum="com.haulmont.cuba.core.global.SendingSt
atus"
width="100%"/>
<hbox spacing="true">
<button caption="Save"
icon="icons/ok.png"/>
<button caption="Reset" icon="font-
icon:REMOVE"/>
</hbox>
</vbox>
</popup>
</popupButton>
Атрибуты popupButton:
autoClose - определяет, должно ли всплывающее меню
закрываться автоматически после вызова действия.
closePopupOnOutsideClick - если установлено значение
true, щелчок по области за пределами всплывающего
меню закрывает его. Это не относится к щелчкам по
самой кнопке компонента.
menuWidth - устанавливает ширину всплывающего
меню.
popupOpenDirection - задаёт направление открытия
всплывающего окна. Возможные значения:
BOTTOM_LEFT,
BOTTOM_RIGHT,
BOTTOM_CENTER.
showActionIcons - разрешает отображение пиктограмм
для кнопок действий.
togglePopupVisibilityOnClick - определяет, должны
ли последовательные щелчки по кнопке компонента
изменять видимость всплывающего меню.
Методы интерфейса PopupButton:
ДЕМОНСТРАЦИЯ
addPopupVisibilityListener() - добавляет компоненту
слушатель для отслеживания событий изменения
видимости компонента.
Атрибуты popupButton popupButton
align - autoClose - caption - closePopupOnOutsideClick - description
- enable - icon - id - menuWidth - popupOpenDirection -
showActionIcons - stylename - tabIndex -
togglePopupVisibilityOnClick - visible - width
Элементы popupButton popupButton
actions - popup
APIAPI
addPopupVisibilityListener
5.5.2.1.30. PopupView5.5.2.1.30. PopupView
PopupView представляет собой
компонент, позволяющий открывать popup с контейнером.
Контейнер может открываться по клику на
минимизированное значение или программно. Он может
быть закрыт отведением курсора или по клику вне
контейнера.
Обычный PopupView со скрытым и видимым popup-ом:
Рисунок 20. Popup 20. Popup скрыт
popupButton.addPopupVisibilityListener(popupVisibili
tyEvent -> {
showNotification("Popup visibility changed");
});
Рисунок 21. Popup 21. Popup открыт
Компонент реализован для блока Web ClientWeb Client.
Пример использования PopupView, где минимизированное
значение получено из пакета локализации:
Содержимое popup-а должно быть контейнером, например
BoxLayout.
Методы PopupView:
setPopupVisible() позволяет открывать и закрывать
popup программно.
setMinimizedValue() позволяет программно менять
минимизированное значение.
<popupView id="popupView"
minimizedValue="msg://minimizedValue"
caption="PopupView caption">
<vbox width="60px" height="40px">
<label value="Content" align="MIDDLE_CENTER"/>
</vbox>
</popupView>
@Inject
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.setPopupVisible(true);
}
@Inject
private PopupView popupView;
addPopupVisibilityListener(PopupVisibilityListener
listener) позволяет отслеживать изменения
видимости popup.
Атрибуты PopupView:
minimizedValue определяет текст минимизированного
значения.
Если атрибуту hideOnMouseOut установлено значение
false, popup будет закрываться по клику вне popup.
captionAsHtml позволяет использовать HTML теги в
подписи компонента.
Атрибуты popupView popupView
caption - captionAsHtml - description - height - hideOnMouseOut -
icon - id - minimizedValue - stylename - visible - width
APIAPI
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.setMinimizedValue("Hello world!");
}
@Inject
private PopupView popupView;
@Override
public void init(Map<String, Object> params) {
popupView.addPopupVisibilityListener(event ->
showNotification(event.isPopupVisible() ?
"The popup is visible" : "The popup is hidden",
NotificationType.HUMANIZED));
}
ДЕМОНСТРАЦИЯ
APIAPI
addPopupVisibilityListener
5.5.2.1.31. ProgressBar5.5.2.1.31. ProgressBar
Компонент ProgressBar служит для
отображения хода выполнения некоторого длительного
процесса.
XML-имя компонента: progressBar
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Пример использования компонента совместно с
механизмом фоновых задач:
<progressBar id="progressBar" width="100%"/>
@Inject
protected ProgressBar progressBar;
@Inject
protected BackgroundWorker backgroundWorker;
private static final int ITERATIONS = 5;
@Override
public void init(Map<String, Object> params) {
BackgroundTask<Integer, Void> task = new
BackgroundTask<Integer, Void>(300, this) {
@Override
public Void run(TaskLifeCycle<Integer>
taskLifeCycle) throws Exception {
Здесь в методе BackgroundTask.progress(), выполняемом
в UI-потоке, компоненту ProgressBar устанавливается
текущее значение. Значением компонента должно быть
число типа float от 0.0 до 1.0.
Изменения значения компонента ProgressBar можно
отслеживать с помощью слушателя ValueChangeListener.
Если выполняемый процесс не может передавать
информацию о прогрессе, то с помощью атрибута
indeterminate можно задать отображение
неопределенного состояния индикатора. Если значение
атрибута равно true, то индикатор отображает
неопределенное состояние. По умолчанию false.
Например:
for (int i = 1; i <= ITERATIONS; i++) {
TimeUnit.SECONDS.sleep(2);
// time
consuming task
taskLifeCycle.publish(i);
}
return null;
}
@Override
public void progress(List<Integer> changes) {
float lastValue =
changes.get(changes.size() - 1);
progressBar.setValue(lastValue /
ITERATIONS);
}
};
BackgroundTaskHandler taskHandler =
backgroundWorker.handle(task);
taskHandler.execute();
}
ДЕМОНСТРАЦИЯ
Например:
По умолчанию неопределённый индикатор представляет
собой горизонтальную полосу. Чтобы отобразить
ProgressBar в виде крутящегося колесика, установите
атрибут stylename="indeterminate-circle".
Чтобы изменить форму индикатора на точку,
перемещающуюся по полосе, вместо растущей полосы,
используйте стиль point:
Атрибуты progressBar progressBar
align - enable - height - id - indeterminate - stylename - visible - width
Предопределенные стили progressBar progressBar
indeterminate-circle - point
APIAPI
addValueChangeListener
5.5.2.1.32. RelatedEntities5.5.2.1.32. RelatedEntities
RelatedEntities - компонент в виде
кнопки с выпадающим списком, при нажатии
показывающим список классов, связанных с сущностью,
экземпляры которой отображаются в таблице.
Пользователь выбирает интересующий его класс
сущности, после чего открывается новый браузер с
экземплярами данной сущности, связанными с
выбранными экземплярами в начальной таблице.
<progressBar id="progressBar" width="100%"
indeterminate="true"/>
progressBar.setStyleName(HaloTheme.PROGRESSBAR_POINT);
XML-имя компонента: relatedEntities
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
При отборе связанных сущностей для отображения
учитываются разрешения пользователя на открытие
экранов, чтение сущностей и чтение атрибутов.
По умолчанию для выбранного в списке класса сущности
открывается браузер сущности, определенный по
соглашениям ({entity_name}.browse,
{entity_name}.lookup). Опционально, экран можно явно
задать в компоненте.
В открытом браузере динамически создается фильтр,
который выбирает связанные с выбранными сущностями
записи.
Пример описания компонента в XML-дескрипторе экрана:
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<buttonsPanel id="buttonsPanel">
<button id="createBtn"
action="invoiceTable.create"/>
<button id="editBtn"
action="invoiceTable.edit"/>
Атрибут for является обязательным. В нем указывается
идентификатор таблицы.
Атрибут openType="NEW_TAB" устанавливает режим
открытия браузера (новая вкладка). По умолчанию
браузер открывается в текущей вкладке.
Элемент property позволяет явно задать связанную
сущность, которая будет отображаться в выпадающем
списке.
Атрибуты property:
name - имя атрибута текущей сущности, ссылающегося
на связанную сущность
screen - идентификатор браузера, открывающегося при
выборе сущности в списке
filterCaption - имя динамически формируемого
фильтра
Атрибут exclude позволяет исключить определенные
связанные сущности из числа отображаемых. В качестве
значения указывается регулярное выражение,
отбирающее ссылочные атрибуты текущей сущности для
action="invoiceTable.edit"/>
<button id="removeBtn"
action="invoiceTable.remove"/>
<relatedEntities for="invoiceTable"
openType="NEW_TAB">
<property name="invoiceItems"
screen="sales$InvoiceItem.lookup"
filterCaption="msg://invoiceItems"/>
</relatedEntities>
</buttonsPanel>
исключения.
В платформе есть API для открытия экранов связанных
сущностей без использования компонента
RelatedEntities: интерфейс RelatedEntitiesAPI и его
реализация RelatedEntitiesBean. Логика задаётся
методом openRelatedScreen(), который принимает
коллекцию сущностей с одной стороны отношения,
MetaClass отдельной сущности из этой коллекции и поле,
являющееся ссылкой на связанные сущности.
<button id="related"
caption="Related customer"
invoke="onRelatedClick"/>
import com.company.sales.entity.Order;
По умолчанию метод открывает стандартный экран
просмотра списка. Дополнительно можно указать
параметр RelatedScreenDescriptor, если требуется
открыть экран, отличный от стандартного, или открыть
его с параметрами. RelatedScreenDescriptor - это
простой Java-объект, хранящий идентификатор экрана
(String), тип его открытия (WindowManager.OpenType),
заголовок фильтра (String) и параметры экрана
(Map<String, Object>).
import com.company.sales.entity.Order;
import
com.haulmont.cuba.gui.components.AbstractLookup;
import com.haulmont.cuba.gui.components.Table;
import
com.haulmont.cuba.gui.relatedentities.RelatedEntitiesA
PI;
import javax.inject.Inject;
public class OrderBrowse extends AbstractLookup {
@Inject
private RelatedEntitiesAPI relatedEntitiesAPI;
@Inject
private Table<Order> ordersTable;
public void onRelatedClick() {
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSe
lected(),
Order.class, "customer");
}
}
ДЕМОНСТРАЦИЯ
(Map<String, Object>).
Атрибуты relatedEntities relatedEntities
align - caption - description - enable - exclude - for - icon - id -
openType - stylename - tabIndex - visible - width
Атрибуты property property
caption - lterCaption - name - screen
5.5.2.1.33. RichTextArea5.5.2.1.33. RichTextArea
Текстовая область для отображения и
ввода форматированного текста.
XML-имя компонента: richTextArea
Компонент RichTextArea реализован только для блока
Web ClientWeb Client.
RichTextArea в основном повторяет функциональность
TextField, за исключением того, что ему нельзя
установить datatype. То есть RichTextArea предназначен
для работы только с текстом и строковыми атрибутами
сущностей.
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSe
lected(),
Order.class, "customer",
new
RelatedScreenDescriptor("sales$Customer.lookup",
OpenType.DIALOG));
ДЕМОНСТРАЦИЯ
Атрибуты richTextArea richTextArea
align - caption - datasource - description - editable - enable - height -
icon - id - property - required - requiredMessage - stylename -
tabIndex - visible - width
APIAPI
addValueChangeListener
5.5.2.1.34. SearchPickerField5.5.2.1.34. SearchPickerField
Компонент SearchPickerField служит
для поиска экземпляров сущностей по вводимой
пользователем строке. Пользователю достаточно ввести
несколько символов и нажать клавишу EnterEnter. Если поиск
дал несколько совпадений, найденные значения
отображаются в виде выпадающего списка. Если же
критерию поиска соответствует только один экземпляр,
он сразу становится значением компонента.
SearchPickerField позволяет также выполнять действия
нажатием на кнопки справа.
См. также SuggestionPickerField.
XML-имя компонента: searchPickerField.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Для работы компонента SearchPickerField необходимо
создать collectionDatasource, и задать в нем запрос,
создать collectionDatasource, и задать в нем запрос,
содержащий условия поиска. Условие обязательно
должно содержать параметр с именем
custom$searchString - именно в него компонент
передает введенную пользователем подстроку при
нажатии EnterEnter. Источник данных с условием поиска
должен быть указан в атрибуте optionsDatasource
компонента. Например:
В данном случае компонент будет искать экземпляры
сущности Colour по вхождению подстроки в ее атрибут
name. Префикс (?i) служит для регистро-независимого
поиска (см. Поиск подстроки без учета регистра).
Выбранное значение подставится в атрибут colour
сущности Car, находящейся в источнике данных carDs.
Атрибут escapeValueForLike со значением true
позволяет искать значения, содержащие специальные
<dsContext>
<datasource id="carDs"
class="com.company.sample.entity.Car"
view="_local"/>
<collectionDatasource id="coloursDs"
class="com.company.sample.entity.Colour"
view="_local">
<query>
select c from sample$Colour c
where c.name like :(?
i)custom$searchString
</query>
</collectionDatasource>
</dsContext>
<layout>
<searchPickerField datasource="carDs"
property="colour" optionsDatasource="coloursDs"/>
позволяет искать значения, содержащие специальные
символы %, \ и _ при помощи like. Чтобы использовать
escapeValueForLike = true, необходимо добавить в
запрос источника данных escape-значение:
Атрибут escapeValueForLike работает со всеми типами
базы данных, кроме HSQLDB.
С помощью атрибута minSearchStringLength можно
задать минимальное количество символов, которое
должен ввести пользователь для поиска значения.
В контроллере экрана для компонента можно
реализовать методы, вызываемые в двух случаях:
если количество введенных символов меньше значения
атрибута minSearchStringLength.
если поиск введенных пользователем символов не дал
результатов.
Пример реализации методов для вывода на экран
сообщений:
select c from ref$Colour c
where c.name like :(?i)custom$searchString or
c.description like :(?i)custom$searchString escape
'\'
@Inject
private SearchPickerField colourField;
@Override
public void init(Map<String, Object> params) {
colourField.setSearchNotifications(new
SearchField.SearchNotifications() {
@Override
public void notFoundSuggestions(String
filterString) {
SearchPickerField реализует интерфейсы LookupField и
PickerField, поэтому все описанное для этих интерфейсов
в части работы с сущностями верно и для него.
Исключением является список действий по умолчанию,
добавляемых при определении компонента в XML: для
SearchPickerField это действия lookup и open .
Атрибуты searchPickerField searchPickerField
align - caption - captionProperty - datasource - description - editable
- enable - lterMode - height - icon - id - inputPrompt - metaClass -
minSearchStringLength - newOptionAllowed - newOptionHandler -
nullName - optionsDatasource - property - required -
requiredMessage - stylename - tabIndex - visible - width
Элементы searchPickerField searchPickerField
actions - validator
Предопределённые стили searchPickerField searchPickerField
filterString) {
showNotification("No colours found for
search string: " + filterString,
NotificationType.TRAY);
}
@Override
public void
needMinSearchStringLength(String filterString, int
minSearchStringLength) {
showNotification("Minimum length of
search string is " + minSearchStringLength,
NotificationType.TRAY);
}
});
}
huge - large
APIAPI
addValueChangeListener
5.5.2.1.35. SideMenu5.5.2.1.35. SideMenu
Компонент SideMenu позволяет создать боковое главное
меню в главном окне приложения, управлять элементами
меню, добавлять пиктограммы и ярлыки и применять
стили.
Его также можно использовать в экранах приложения как
обычный визуальный компонент. Для этого необходимо
добавить пространство имён
xmlns:main="http://schemas.haulmont.com/cuba/mainwi
ndow.xsd" в дескриптор экрана.
XML-имя компонента: sideMenu.
Пример описания компонента в XML-дескрипторе экрана:
<main:sideMenu id="sideMenu"
CUBA Studio предоставляет готовый шаблон главного
экрана с реализацией компонента sideMenu и готовыми
стилями боковой панели:
<main:sideMenu id="sideMenu"
width="100%"
selectOnClick="true"/>
<layout>
<hbox id="horizontalWrap"
expand="workArea"
height="100%"
stylename="c-sidemenu-layout"
width="100%">
<vbox id="sideMenuPanel"
expand="sideMenu"
height="100%"
margin="false,false,true,false"
spacing="true"
stylename="c-sidemenu-panel"
width="250px">
<hbox id="appTitleBox"
spacing="true"
stylename="c-sidemenu-title"
width="100%">
<label id="appTitleLabel"
align="MIDDLE_CENTER"
value="mainMsg://application.logoLabel"/>
</hbox>
<embedded id="logoImage"
align="MIDDLE_CENTER"
stylename="c-app-icon"
type="IMAGE"/>
<hbox id="userInfoBox"
align="MIDDLE_CENTER"
expand="userIndicator"
expand="userIndicator"
margin="true"
spacing="true"
width="100%">
<main:userIndicator id="userIndicator"
align="MIDDLE_CENTER"/>
<main:newWindowButton
id="newWindowButton"
description="mainMsg://newWindowBtnDescription"
icon="app/images/new-window.png"/>
<main:logoutButton id="logoutButton"
description="mainMsg://logoutBtnDescription"
icon="app/images/exit.png"/>
</hbox>
<main:sideMenu id="sideMenu"
width="100%"/>
<main:ftsField id="ftsField"
width="100%"/>
</vbox>
<main:workArea id="workArea"
height="100%">
<main:initialLayout margin="true"
spacing="true">
<label id="welcomeLabel"
align="MIDDLE_CENTER"
stylename="c-welcome-text"
value="mainMsg://application.welcomeText"/>
</main:initialLayout>
</main:workArea>
</hbox>
</layout>
Атрибуты sideMenu: sideMenu:
selectOnClick - установка атрибута в true
подсвечивает выделение элемента меню после его
выбора кликом мыши. По умолчанию false.
Методы интерфейса SideMenu: SideMenu:
createMenuItem - создаёт новый объект элемента меню,
но не добавляет его к меню. Идентификатор id должен
быть уникальным в области всего меню.
addMenuItem - добавляет элемент к меню.
removeMenuItem - удаляет элемент из списка элементов
меню.
getMenuItem - возвращает объект элемента меню по его
идентификатору.
hasMenuItems - возвращает true, если в меню есть
вложенные элементы.
Компонент SideMenu предназначен для отображения
элементов меню. Чтобы создать элемент меню,
используется API компонента MenuItem в контроллере
экрана. Методы, перечисленные ниже, можно
использовать для динамического обновления элементов
меню, реализуя бизнес-логику приложения. Пример
программного создания элемента меню:
</layout>
Методы интерфейса MenuItem: MenuItem:
setCaption - устанавливает заголовок элемента меню.
setCaptionAsHtml - разрешает/запрещает
использование HTML-заголовков.
setBadgeText - устанавливает текст ярлыка элемента
меню. Ярлыки представляют собой небольшие виджеты
справа от элемента меню, к примеру:
SideMenu.MenuItem item =
sideMenu.createMenuItem("special");
item.setCaption("Daily offer");
item.setBadgeText("New");
item.setIcon("font-icon:GIFT");
sideMenu.addMenuItem(item,0);
int count = 5;
SideMenu.MenuItem item =
sideMenu.createMenuItem("count");
item.setCaption("Messages");
item.setBadgeText(count + " new");
item.setIcon("font-icon:ENVELOPE");
sideMenu.addMenuItem(item,0);
Текст ярлыка можно обновлять автоматически с
помощью компонента Timer:
setIcon - устанавливает пиктограмму элемента меню.
setCommand - используется для описания действия,
которое должно быть выполнено при выборе этого
public void updateCounters(Timer source) {
sideMenu.getMenuItemNN("sales")
.setBadgeText(String.valueOf(LocalTime.MIDNIGHT.minu
sSeconds(timerCounter-source.getDelay())));
timerCounter++;
}
которое должно быть выполнено при выборе этого
элемента меню кликом мыши.
addChildItem/removeChildItem - добавляет/удаляет
элементы меню в подгруппу корневого элемента.
setExpanded - раскрывает или сворачивает подгруппы
меню по умолчанию.
setStyleName - устанавливает один или более
пользовательских стилей для компонента, заменяя все
ранее заданные стили. Имена стилей при перечислении
отделаются пробелами. Имя стиля должно быть
названием существующего CSS-класса.
Стандартный шаблон главного экрана с sideMenu
стилизован несколькими предопределёнными стилями:
c-sidemenu-layout, c-sidemenu-panel и c-sidemenu-
title. Стиль бокового меню по умолчанию c-sidemenu
поддерживается только в рамках темы Halo и темах, её
расширяющих. В теме Havana стили sideMenu не
поддерживаются.
setTestId - устанавливает значение cuba-id для
тестирования UI.
Атрибуты sideMenu sideMenu
align - caption - description - enable - height - icon - id -
selectOnClick - stylename - tabIndex - visible - width
Атрибуты ftsfield ftsfield
align - caption - description - enable - height - icon - id - stylename -
visible - width
API sideMenuAPI sideMenu
addMenuItem - createMenuItem - removeMenuItem
API menuItemAPI menuItem
addChildItem - removeChildItem - setBadgeText - setCaption -
setCaptionAsHtml - setCommand - setExpanded - setIcon -
setStyleName - setTestId
ДЕМОНСТРАЦИЯ
setStyleName - setTestId
5.5.2.1.36. SourceCodeEditor5.5.2.1.36. SourceCodeEditor
SourceCodeEditor - компонент для
отображения и ввода исходного кода. Он представляет
собой многострочное текстовое поле с возможностью
подсветки кода и отображения полей печати и номеров
строк.
XML-имя компонента: sourceCodeEditor.
Компонент SourceCodeEditor реализован для блока WebWeb
ClientClient.
SourceCodeEditor в основном повторяет
функциональность TextField и имеет следующие
специфические атрибуты:
если handleTabKey имеет значение true, нажатие на
кнопку TabTab на клавиатуре добавляет отступ текущей
строки, если значение равно false, нажатие
перемещает курсор или фокус на следующую позицию
табуляции. Данный атрибут необходимо установить во
время инициализации экрана, он не может быть
изменён во время работы.
Следующие свойства можно легко изменять в
работающем приложении:
highlightActiveLine используется для подсветки
текущей строки, на которой находится курсор.
атрибут mode предоставляет список языков, для которых
поддерживается подсветка синтаксиса. Этот список
задан в перечислении Mode интерфейса
SourceCodeEditor и включает в себя следующие языки:
Java, HTML, XML, Groovy, SQL, JavaScript, Properties и Text без
подсветки.
printMargin определяет, отображать или скрыть линию
printMargin определяет, отображать или скрыть линию
края печати в текстовом поле.
showGutter используется для отображения или скрытия
левой панели с номерами строк.
Ниже приведён пример компонента SourceCodeEditor с
динамически настраиваемыми атрибутами.
XML-дескриптор:
Контроллер:
<hbox spacing="true">
<checkBox id="highlightActiveLineCheck"
align="BOTTOM_LEFT" caption="Highlight Active Line"/>
<checkBox id="printMarginCheck"
align="BOTTOM_LEFT" caption="Print Margin"/>
<checkBox id="showGutterCheck" align="BOTTOM_LEFT"
caption="Show Gutter"/>
<lookupField id="modeField" align="BOTTOM_LEFT"
caption="Mode" required="true"/>
</hbox>
<sourceCodeEditor id="simpleCodeEditor" width="100%"/>
@Inject
private LookupField modeField;
@Inject
private SourceCodeEditor simpleCodeEditor;
@Inject
private CheckBox highlightActiveLineCheck;
@Inject
private CheckBox printMarginCheck;
@Inject
private CheckBox showGutterCheck;
@Override
public void init(Map<String, Object> params) {
Результат выполения кода:
public void init(Map<String, Object> params) {
highlightActiveLineCheck.setValue(simpleCodeEditor.isH
ighlightActiveLine());
highlightActiveLineCheck.addValueChangeListener(e
->
simpleCodeEditor.setHighlightActiveLine(Boolean.TRUE.e
quals(e.getValue())));
printMarginCheck.setValue(simpleCodeEditor.isShowPrint
Margin());
printMarginCheck.addValueChangeListener(e ->
simpleCodeEditor.setShowPrintMargin(Boolean.TRUE.equal
s(e.getValue())));
showGutterCheck.setValue(simpleCodeEditor.isShowGutter
());
showGutterCheck.addValueChangeListener(e ->
simpleCodeEditor.setShowGutter(Boolean.TRUE.equals(e.g
etValue())));
Map<String, Object> modes = new HashMap<>();
for (SourceCodeEditor.Mode mode :
SourceCodeEditor.Mode.values()) {
modes.put(mode.toString(), mode);
}
modeField.setOptionsMap(modes);
modeField.setValue(SourceCodeEditor.Mode.Text);
modeField.addValueChangeListener(e ->
simpleCodeEditor.setMode((SourceCodeEditor.Mode)
e.getValue()));
}
Результат выполения кода:
Компонент SourceCodeEditor также поддерживает
автодополнение кода, определяемое с помощью класса
Suggester. Чтобы подключить автодополнение,
необходимо переопределить и вызвать метод
setSuggester, например:
@Inject
private SourceCodeEditor suggesterCodeEditor;
@Inject
private CollectionDatasource<User, UUID> usersDs;
@Override
public void init(Map<String, Object> params) {
suggesterCodeEditor.setSuggester((source, text,
cursorPosition) -> {
List<Suggestion> suggestions = new ArrayList<>
();
usersDs.refresh();
for (User user : usersDs.getItems()) {
suggestions.add(new Suggestion(source,
user.getLogin(), user.getName(), null, -1, -1));
}
ДЕМОНСТРАЦИЯ
Результат:
Атрибуты sourceCodeEditor sourceCodeEditor
align - caption - colspan - datasource - description - editable - enable
- handleTabKey - height - highlightActiveLine - icon - id - mode -
printMargin - property - required - requiredMessage - rowspan -
showGutter - stylename - tabIndex - visible - width
APIAPI
addValueChangeListener
5.5.2.1.37. SuggestionPickerField5.5.2.1.37. SuggestionPickerField
Компонент SuggestionPickerField
предназначен для поиска экземпляров сущности по
строке, вводимой пользователем. Он отличается от
SearchPickerField тем, что обновляет список опций при
каждом вводе символа пользователем без необходимости
нажимать Enter. Список опций загружается асинхронно в
соответствии с логикой, задаваемой разработчиком на
стороне сервера.
SuggestionPickerField является также PickerField и может
содержать действия, отображаемые кнопками справа.
}
return suggestions;
});
}
XML-имя компонента: suggestionPickerField.
Компонент реализован для блока Web ClientWeb Client.
SuggestionPickerField используется для выбора
значений ссылочных атрибутов, поэтому для компонента
обычно указываются атрибуты datasource и property:
Атрибуты suggestionPickerField:
asyncSearchDelayMs - устанавливает задержку между
последним нажатием клавиши и асинхронным поиском.
metaClass - указывает ссылку на интерфейс метаданных
<dsContext>
<datasource id="orderDs"
class="com.company.sample.entity.Order"
view="order-view"/>
</dsContext>
<layout>
<suggestionPickerField id="suggestionPickerField"
captionProperty="name"
datasource="orderDs"
property="customer"/>
</layout>
metaClass - указывает ссылку на интерфейс метаданных
компонента в случае, если компонент используется без
непосредственной привязки к данным, то есть без
указания datasource и property.
minSearchStringLength - устанавливает минимальную
длину строки для начала поиска.
suggestionsLimit - устанавливает ограничение
количества выводимых подсказок.
Элементы suggestionPickerField:
actions - необязательный элемент для описания
действий, связанных с компонентом. Кроме описания
произвольных действий, поддерживаются следующие
стандартные действия, определяемые перечислением
PickerField.ActionType: lookup, open, clear.
query - необязательный элемент, позволяющий задать
запрос для выбора предлагаемых значений. Элемент
query, в свою очередь, имеет следующие атрибуты:
entityClass (обязательный атрибут) - полное
квалифицированное имя класса сущности.
escapeValueForLike - позволяет разрешить поиск по
значениям, содержащим специальные символы: %, \, и
т.д. По умолчанию false,
searchStringFormat - строка Groovy, что позволяет
использовать в запросе валидные Groovy-выражения.
<suggestionField id="suggestionField"
captionProperty="login">
<query
entityClass="com.haulmont.cuba.security.entity.User"
escapeValueForLike="true"
searchStringFormat="%$searchString%">
select e from sec$User e where e.login like
:searchString
Если элемент query не задан, то список опций должен
быть предоставлен объектом типа SearchExecutor,
созданным программно (см. ниже).
Простой пример использования SuggestionPickerField SuggestionPickerField
Как правило, для компонента достаточно установить
SearchExecutor. SearchExecutor - это
функциональный интерфейс, содержащий один метод:
List<E extends Entity> search(String
searchString, Map<String, Object> searchParams):
Метод search() выполняется в фоновом потоке,
поэтому он не может обращаться к визуальным
компонентам или источникам данных, связанным
с визуальными компонентами. Можно
использовать DataManager или напрямую вызывать
сервисы среднего слоя, или обрабатывать и
возвращать данные, предварительно
загруженные в экран.
Параметр searchString может быть использован для
фильтрации кандидатов по строке, введенной
пользователем:
:searchString
</query>
</suggestionField>
suggestionPickerField.setSearchExecutor((searchStri
ng, searchParams) -> {
return Arrays.asList(entity1, entity2, ...);
});
public class OrderEdit extends
AbstractEditor<Order> {
Использование ParametrizedSearchExecutor ParametrizedSearchExecutor
В примерах выше параметр searchParams является
пустым. Для поиска с параметрами используется
ParametrizedSearchExecutor:
AbstractEditor<Order> {
@Inject
private SuggestionPickerField
suggestionPickerField;
@Inject
private DataManager dataManager;
@Override
public void init(Map<String, Object> params) {
suggestionPickerField.setSearchExecutor((searchStri
ng, searchParams) ->
dataManager.loadList(LoadContext.create(Customer.cl
ass).setQuery(
LoadContext.createQuery("select c from
sample$Customer c where c.name like :name order by
c.name")
.setParameter("name", "%"
+ searchString + "%"))));
}
}
suggestionPickerField.setSearchExecutor(new
SuggestionField.ParametrizedSearchExecutor<Customer
>(){
@Override
public Map<String, Object> getParams() {
Использование EnterActionHandler EnterActionHandler и ArrowDownActionHandler ArrowDownActionHandler
Компонент также может быть использован с
обработчиками событий EnterActionHandler и
ArrowDownActionHandler. Эти листнеры срабатывают,
когда пользователь нажимает клавиши EnterEnter или ArrowArrow
DownDown при скрытом всплывающем окне для подсказок.
Они также представляют собой функциональные
интерфейсы с единственным методом с одним
параметром - currentSearchString. Вы можете
настроить и свои обработчики событий и использовать
метод showSuggestions() интерфейса
SuggestionField, который принимает список
сущностей, для отображения подсказок:
return ParamsMap.of(...);
}
@Override
public List<Customer> search(String
searchString, Map<String, Object> searchParams) {
return executeSearch(searchString,
searchParams);
}
});
suggestionPickerField.setArrowDownActionHandler(cur
rentSearchString -> {
List<Customer> suggestions =
findSuggestions();
suggestionPickerField.showSuggestions(suggestions);
});
suggestionPickerField.setEnterActionHandler(current
SearchString -> {
ДЕМОНСТРАЦИЯ
Атрибуты suggestionPickerField suggestionPickerField
align - asyncSearchDelayMs - caption - captionProperty - colspan -
datasource - description - editable - enable - height - icon - id -
inputPrompt - metaClass - minSearchStringLength - property -
required - requiredMessage - responsive - rowspan - stylename -
suggestionsLimit - tabIndex - visible - width
Элементы suggestionPickerField suggestionPickerField
actions - query - validator
Атрибуты query query
entityClass - escapeValueForLike - searchStringFormat
APIAPI
addValueChangeListener
5.5.2.1.38. Table5.5.2.1.38. Table
Компонент Table позволяет выводить
информацию в табличном виде, сортировать данные,
управлять колонками и заголовками таблицы, вызывать
действия для выбранных строк.
XML-имя компонента: table
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
SearchString -> {
List<Customer> suggestions =
getDefaultSuggestions();
suggestionPickerField.showSuggestions(suggestions);
});
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Пример описания таблицы в XML-дескрипторе экрана:
Здесь в элементе dsContext определен источник данных
collectionDatasource, который выбирает сущности Order
с помощью JPQL запроса select o from sales$Order o
order by o.date. Для компонента table в элементе rows
указывается используемый источник данных, а в
элементе columns - какие атрибуты сущности,
содержащейся в источнике данных, использовать в
качестве колонок.
Элементы table:
<dsContext>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order"
view="orderWithCustomer">
<query>
select o from sales$Order o order by
o.date
</query>
</collectionDatasource>
</dsContext>
<layout>
<table id="ordersTable" width="300px">
<columns>
<column id="date"/>
<column id="customer.name"/>
<column id="amount"/>
</columns>
<rows datasource="ordersDs"/>
</table>
rows - обязательный элемент, в атрибуте datasource
которого необходимо объявить используемый таблицей
источник данных.
Для строк можно настроить отображение заголовков -
задать каждой строке свой значок в дополнительной
колонке слева. Для этого в контроллере экрана
необходимо реализовать интерфейс
ListComponent.IconProvider и установить его таблице:
columns - обязательный элемент, определяет набор
колонок таблицы.
@Inject
private Table<Customer> table;
@Override
public void init(Map<String, Object> params) {
table.setIconProvider(new
ListComponent.IconProvider<Customer>() {
@Nullable
@Override
public String getItemIcon(Customer entity) {
CustomerGrade grade = entity.getGrade();
switch (grade) {
case PREMIUM: return
"icons/premium_grade.png";
case HIGH: return
"icons/high_grade.png";
case MEDIUM: return
"icons/medium_grade.png";
default: return null;
}
}
});
}
колонок таблицы.
Каждая колонка описывается во вложенном элементе
column со следующими атрибутами:
id − обязательный атрибут, содержит название
атрибута сущности, выводимого в колонке. Может быть
как непосредственным атрибутом сущности,
находящейся в источнике данных, так и атрибутом
связанной сущности - переход по графу объектов
обозначается точкой. Например:
caption − необязательный атрибут, содержит
заголовок колонки. Если не задан, будет отображено
локализованное название атрибута сущности.
collapsed − необязательный атрибут, при указании
true колонка будет изначально скрыта. Пользователь
может управлять отображением колонок с помощью
меню, доступного по кнопке в правой верхней части
таблицы, если атрибут columnControlVisible таблицы
не false. По умолчанию collapsed имеет значение
false.
width − необязательный атрибут, отвечает за
изначальную ширину колонки. Может принимать
только числовые значения в пикселах.
align - необязательный атрибут, устанавливает
выравнивание текста в ячейках данной колонки.
Возможные значения: LEFT, RIGHT, CENTER. По
умолчанию LEFT.
<columns>
<column id="date"/>
<column id="customer"/>
<column id="customer.name"/>
<column id="customer.address.country"/>
</columns>
умолчанию LEFT.
editable − необязательный атрибут,
разрешает/запрещает редактирование данной колонки
в редактируемой таблице. Чтобы колонка была
редактируемой, атрибут editable всей таблицы также
должен быть установлен в true. Динамическое
изменение значения этого атрибута не
поддерживается.
sortable − необязательный атрибут, позволяющий
запретить сортировку колонки. Вступает в действие,
если атрибут sortable всей таблицы установлен в true
(что имеет место по умолчанию).
maxTextLength - необязательный атрибут, позволяет
ограничивать количество символов в ячейке. При этом
если разница между фактическим и допустимым
количеством символов не превышает порог в 10
символов, "лишние" символы не скрываются. Для
просмотра полной записи надо кликнуть на ее видимую
часть. Пример колонки с ограничением в 5 символов:
link - установка атрибута в true позволяет отобразить
в ячейке таблицы ссылку на экран просмотра
экземпляра сущности (поддерживается только для WebWeb
ClientClient). Атрибут link="true") может указываться и для
колонок примитивных типов: в этом случае, при
нажатии на ссылку будет открываться редактор
основной сущности таблицы. Такой подход может
применяться для упрощения навигации - пользователи
смогут открывать редактор одним кликом по
смогут открывать редактор одним кликом по
некоторому ключевому атрибуту.
linkScreen - позволяет указать идентификатор экрана,
который будет открыт по нажатию на ссылку,
включенную свойством link.
linkScreenOpenType - задает режим открытия экрана
(THIS_TAB, NEW_TAB или DIALOG).
linkInvoke - позволяет заменить открытие окна на
вызов метода контроллера.
captionProperty - имя атрибута сущности, который
должен быть отображен в колонке вместо указанного в
id. Например, если имеется связанная сущность
Priority с атрибутами name и orderNo, можно
определить следующую колонку:
В этом случае в колонке будет отображаться название
приоритета, а сортировка колонки будет
осуществляться по атрибуту orderNo.
необязательный атрибут generator содержит ссылку
на метод в контроллере экрана, который создает
визуальный компонент для отображения содержимого
ячейки:
public void linkedMethod(Entity item, String
columnId) {
Customer customer = (Customer) item;
showNotification(customer.getName());
}
<column id="priority.orderNo"
captionProperty="priority.name"
caption="msg://priority" />
Он может быть использован вместо передачи
реализации Table.ColumnGenerator в метод
addGeneratedColumn().
Элемент column может содержать вложенный элемент
formatter для представления значения атрибута в виде,
отличном от стандартного для данного Datatype:
<columns>
<column id="name"/>
<column id="imageFile"
generator="generateImageFileCell"/>
</columns>
public Component generateImageFileCell(Employee
entity) {
Embedded embedded =
componentsFactory.createComponent(Embedded.class);
embedded.setType(Embedded.Type.IMAGE);
FileDescriptor userImageFile =
entity.getImageFile();
FileDataProvider dataProvider = new
FileDataProvider(userImageFile);
embedded.setSource(userImageFile.getId() + "."
+ userImageFile.getExtension(), dataProvider);
return embedded;
}
<column id="date">
<formatter
class="com.haulmont.cuba.gui.components.formatters.
DateFormatter"
format="yyyy-MM-dd HH:mm:ss"/>
</column>
rowsCount − необязательный элемент, создающий для
таблицы компонент RowsCount, который позволяет
загружать в таблицу данные постранично. Размер
страницы задается путем ограничения количества
записей в источнике данных методом
CollectionDatasource.setMaxResults(). Как правило,
это делает связанный с источником данных таблицы
компонент Filter, однако при отсутствии универсального
фильтра можно вызвать этот метод и напрямую из
контроллера экрана.
Компонент RowsCount может также отобразить общее
число записей, возвращаемых текущим запросом в
источнике данных, без извлечения этих записей. Для
этого при щелчке пользователя на знаке "?" он вызывает
метод AbstractCollectionDatasource.getCount(), что
приводит к выполнению в БД запроса с такими же, как у
текущего запроса условиями, но с агрегатной функцией
COUNT(*) вместо результатов. Полученное число
отображается вместо знака "?".
actions − необязательный элемент для описания
действий, связанных с таблицей. Кроме описания
произвольных действий, поддерживаются следующие
стандартные действия, определяемые перечислением
ListActionType: create, edit, remove, refresh, add,
exclude, excel.
buttonsPanel - необязательный элемент, создающий
над таблицей контейнер ButtonsPanel для отображения
кнопок действий.
Атрибуты table:
Атрибут multiselect позволяет задать режим
множественного выделения строк в таблице. Если
multiselect равен true, то пользователь может
multiselect равен true, то пользователь может
выделить несколько строк с помощью клавиатуры или
мыши, удерживая клавиши CtrlCtrl или ShiftShift. По умолчанию
режим множественного выделения отключен.
Атрибут sortable разрешает или запрещает сортировку
в таблице. По умолчанию имеет значение true. Если
сортировка разрешена, то при нажатии на название
колонки справа от названия появляется значок / .
Сортировку некоторой отдельной колонки можно
запретить с помощью атрибута sortable этой колонки.
При включенной с помощью элемента rowsCount (см.
выше) страничной загрузке таблицы сортировка
производится разными способами в зависимости от
того, умещаются ли все записи на одной странице. Если
умещаются, то сортировка производится в памяти, без
обращений к БД. Если же страниц больше одной, то
сортировка производится на базе данных путем
отправки нового запроса с соответствующим ORDER BY.
Колонка таблицы может ссылаться на локальный
атрибут или на связанную сущность. Например:
В последнем случае, сортировка на базе данных
производится по атрибутам, указанным в аннотации
@NamePattern связанной сущности. Если у связанной
<table id="ordersTable">
<columns>
<column id="customer.name"/>
<!-- the 'name'
attribute of the 'Customer' entity -->
<column id="contract"/>
<!-- the
'Contract' entity -->
</columns>
<rows datasource="ordersDs"/>
</table>
@NamePattern связанной сущности. Если у связанной
сущности нет такой аннотации, то сортировка
производится в памяти только в пределах текущей
страницы.
Если колонка таблицы ссылается на неперсистентный
атрибут, то сортировка на базе данных производится по
атрибутам, указанным в параметре related()
аннотации @MetaProperty. Если такой параметр не
указан, то сортировка производится в памяти только в
пределах текущей страницы.
Если таблица соединена со вложенным источником
данных, который содержит коллекцию связанных
сущностей, то для того, чтобы таблицу можно было
сортировать, атрибут-коллекция должен быть
упорядоченного типа (List или LinkedHashSet). Если
атрибут имеет тип Set, то атрибут sortable не
оказывает влияния и пользователи не смогут
сортировать таблицу.
Атрибут presentations управляет механизмом
представлений. Значение по умолчанию равно false.
Когда значение атрибута равно true, то в верхнем
правом углу таблицы появляется значок . Механизм
представлений реализован только для блока Web ClientWeb Client.
Установка атрибута columnControlVisible в false
запрещает пользователю скрывать колонки с помощью
меню, выпадающего при нажатия на кнопку в правой
части шапки таблицы. Флажками в меню отмечаются
отображаемые в данный момент колонки.
Установка атрибута reorderingAllowed в false
запрещает пользователю менять местами колонки,
перетаскивая их с помощью мыши.
Установка атрибута columnHeaderVisible в false
скрывает заголовок таблицы.
При установленном в false атрибуте showSelection
текущая строка не имеет выделения.
Атрибут contextMenuEnabled разрешает или запрещает
показывать контекстное меню. По умолчанию атрибут
имеет значение true. В контекстном меню
отображаются действия таблицы (если они есть), и
пункт Системная информация, содержащий
информацию о выбранной сущности (если у
пользователя есть разрешение cuba.gui.showInfo).
Если атрибуту multiLineCells таблицы присвоить
значение true, то ячейки, содержащие текст с
переносами строк, будут отображать его в несколько
строк. В таком режиме в веб клиенте для правильной
работы полосы прокрутки все строки текущей страницы
таблицы будут загружены веб-браузером сразу, без
ленивой загрузки видимой части таблицы. По
умолчанию атрибут имеет значение false.
Атрибут aggregatable включает режим агрегации строк
таблицы. Поддерживаются следующие операции:
SUM - сумма
AVG - среднее значение
COUNT - количество
MIN - минимальное значение
MAX - максимальное значение
Для агрегируемых колонок необходимо указать
элемент aggregation с атрибутом type, задающим
функцию агрегации. По умолчанию в агрегируемых
функцию агрегации. По умолчанию в агрегируемых
колонках поддерживаются только числовые типы
данных, такие как Integer, Double, Long и BigDecimal.
Агрегированные значения столбцов выводятся в
дополнительной строке вверху таблицы. Пример
описания таблицы с агрегацией:
Элемент aggregation может также содержать атрибут
strategyClass, указывающий класс, реализующий
интерфейс AggregationStrategy interface (см. ниже
пример установки стратегии агрегации программно).
Для отображения агрегированного значения в виде,
отличном от стандартного для данного Datatype, для
него можно указать Formatter:
Атрибут aggregationStyle позволяет задать положение
строки агрегации: TOP или BOTTOM. По умолчанию
<table id="itemsTable" aggregatable="true">
<columns>
<column id="product"/>
<column id="quantity"/>
<column id="amount">
<aggregation type="SUM"/>
</column>
</columns>
<rows datasource="itemsDs"/>
</table>
<column id="amount">
<aggregation type="SUM">
<formatter
class="com.company.sample.MyFormatter"/>
</aggregation>
</column>
строки агрегации: TOP или BOTTOM. По умолчанию
используется TOP.
В дополнение к операциям, перечисленным выше,
можно задать собственную стратегию агрегации путем
создания класса, реализующего интерфейс
AggregationStrategy, и передачи его методу
setAggregation() класса Table.Column в составе
экземпляра AggregationInfo. Например:
public class TimeEntryAggregation implements
AggregationStrategy<List<TimeEntry>, String> {
@Override
public String
aggregate(Collection<List<TimeEntry>>
propertyValues) {
HoursAndMinutes total = new
HoursAndMinutes();
for (List<TimeEntry> list : propertyValues)
{
for (TimeEntry timeEntry : list) {
total.add(HoursAndMinutes.fromTimeEntry(timeEntry));
}
}
return
StringFormatHelper.getTotalDayAggregationString(tota
l);
}
@Override
public Class<String> getResultClass() {
return String.class;
}
}
AggregationInfo info = new AggregationInfo();
Атрибут editable позволяет перевести таблицу в
режим in-place редактирования ячеек. В этом режиме в
колонках, имеющих атрибут editable = true,
отображаются компоненты для редактирования
значений атрибутов сущности, находящейся в
источнике данных.
Тип компонента для каждой редактируемой колонки
выбирается автоматически на основании типа атрибута
сущности. Например, для строковых и числовых
атрибутов используется TextField, для Date - DateField, для
перечислений - LookupField, для ссылок на другие
сущности - PickerField.
Для редактируемой колонки типа Date можно
дополнительно указать атрибуты dateFormat или
resolution аналогично описанным для DateField.
Для редактируемой колонки, отображающей связанную
сущность, можно дополнительно указать атрибуты
optionsDatasource и captionProperty. При указании
optionsDatasource вместо PickerField используется
компонент LookupField.
Произвольно настроить отображение ячеек, в том числе
для редактирования содержимого, можно с помощью
метода Table.addGeneratedColumn() - см. ниже.
В веб-клиенте с темой, основанной на Halo, атрибут
stylename позволяет применять к таблице
предопределенные стили Table. Стили задаются в XML-
AggregationInfo info = new AggregationInfo();
info.setPropertyPath(metaPropertyPath);
info.setStrategy(new TimeEntryAggregation());
Table.Column column =
weeklyReportsTable.getColumn(columnId);
column.setAggregation(info);
дексрипторе или контроллере экрана с помощью
атрибута stylename:
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
TABLE_:
Стили компонента Table:
borderless - удаляет внешнюю рамку таблицы.
compact - уменьшает отступы внутри ячеек таблицы.
no-header - скрывает заголовки таблицы.
no-horizontal-lines - удаляет горизонтальные
строковые разделители.
no-stripes - отключает чередование цветов строк
таблицы.
no-vertical-lines - удаляет вертикальные
разделители столбцов.
small - уменьшает размер шрифта и отступы внутри
ячеек таблицы.
Методы интерфейса Table:
метод addColumnCollapsedListener() позволяет
<table id="table"
stylename="no-stripes">
<columns>
<column id="product"/>
<column id="quantity"/>
</columns>
<rows datasource="itemsDs"/>
</table>
table.setStyleName(HaloTheme.TABLE_NO_STRIPES);
метод addColumnCollapsedListener() позволяет
отслеживать видимость колонок таблицы с помощью
интерфейса слушателя ColumnCollapsedListener.
getSelected(), getSingleSelected() - возвращают
экземпляры сущностей, соответствующие выделенным
в таблице строкам. Коллекцию можно получить
вызовом метода getSelected(). Если ничего не выбрано,
возвращается пустой набор. Если multiselect
отключен, удобно пользоваться методом
getSingleSelected(), возвращающим одну выбранную
сущность или null, если ничего не выбрано.
Метод addGeneratedColumn() позволяет задать
собственное представление данных в колонке. Он
принимает два параметра: идентификатор колонки и
реализацию интерфейса Table.ColumnGenerator.
Идентификатор может совпадать с одним из
идентификаторов, указанных для колонок таблицы в
XML-дескрипторе - в этом случае новая колонка
вставляется вместо заданной в XML. Если
идентификатор не совпадает ни с одной колонкой,
создается новая справа.
Метод generateCell() интерфейса
Table.ColumnGenerator вызывается таблицей для
каждой строки, и в него передается экземпляр
сущности, отображаемой в данной строке. Метод
generateCell() должен вернуть визуальный
компонент, который и будет отображаться в ячейке.
Пример использования:
@Inject
protected Table carsTable;
@Inject
protected ComponentsFactory componentsFactory;
В данном случае в ячейках колонки colour таблицы
отображается компонент LookupPickerField. Компонент
должен сохранять свое значение в атрибут colour
сущности, экземпляр которой отображается в данной
строке. Для этого у таблицы методом
getItemDatasource() запрашивается источник данных
для текущего экземпляра сущности, и передается
компоненту LookupPickerField.
Если в ячейке необходимо отобразить просто
динамически сформированный текст, вместо
компонента Label используйте класс
Table.PlainTextCell. Это упростит отрисовку и
сделает таблицу быстрее.
protected ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
carsTable.addGeneratedColumn("colour", new
Table.ColumnGenerator() {
@Override
public Component generateCell(Entity entity)
{
LookupPickerField field =
componentsFactory.createComponent(LookupPickerField.
NAME);
field.setDatasource(carsTable.getItemDatasource(enti
ty), "colour");
field.setOptionsDatasource(coloursDs);
field.addLookupAction();
field.addOpenAction();
return field;
}
});
}
сделает таблицу быстрее.
Если в метод addGeneratedColumn() передан
идентификатор колонки, не объявленной в XML-
дескрипторе, то может понадобиться установить
заголовок новой колонки следующим образом:
Существует также более декларативный подход,
использующий XML-атрибут generator.
Метод requestFocus() позволяет установить фокус на
определенное поле конкретной записи. Принимает два
параметра: экземпляр сущности, определяющй строку и
идентификатор колонки. Пример программной
установки фокуса:
Метод scrollTo() позволяет программно прокрутить
таблицу до нужной записи. Метод принимает экземпляр
сущности, определяющий нужную строку в таблице.
Пример использования метода:
Метод setClickListener() может избавить от
необходимости добавлять генерируемые колонки с
компонентами, если нужно нарисовать что-либо в
ячейках и получать оповещения когда пользователь
кликает на эти ячейки. Имплементация класса
CellClickListener, передаваемая в данный метод,
получает текущий экземпляр сущности и
идентификатор колонки. Содержимое ячеек будет
carsTable.getColumn("colour").setCaption("Colour");
table.requestFocus(item, "count");
table.scrollTo(item);
идентификатор колонки. Содержимое ячеек будет
завернуто в элемент span со стилем cuba-table-
clickable-cell, который можно использовать для
задания отображения ячеек.
Метод setStyleProvider() позволяет задать стиль
отображения ячеек таблицы. Параметром метода
должна быть реализация интерфейса
Table.StyleProvider. Метод getStyleName() этого
интерфейса вызывается таблицей отдельно для каждой
строки и для каждой ячейки. Если метод вызван для
строки, то первый параметр содержит экземпляр
сущности, отображаемый этой строкой, а второй
параметр null. Если же метод вызван для ячейки, то
второй параметр содержит имя атрибута,
отображаемого этой ячейкой.
Пример задания стилей:
@Inject
protected Table customersTable;
@Override
public void init(Map<String, Object> params) {
customersTable.setStyleProvider(new
Table.StyleProvider() {
@Nullable
@Override
public String getStyleName(Entity entity,
@Nullable String property) {
Customer customer = (Customer) entity;
if (property == null) {
// style for row
if (hasComplaints(customer)) {
return"unsatisfied-customer";
}
} else if (property.equals("grade")) {
Далее нужно определить заданные для строк и ячеек
стили в теме приложения. Подробная информация о
создании темы находится в Создание темы
приложения. Для веб-клиента новые стили
определяются в файле styles.scss. Имена стилей,
заданные в контроллере, совместно с префиксами,
обозначающими строку или колонку таблицы, образуют
CSS-селекторы. Например:
// style for column "grade"
switch (customer.getGrade()) {
case PREMIUM: return "premium-
grade";
case HIGH: return "high-grade";
case MEDIUM: return "medium-
grade";
default: return null;
}
}
return null;
}
});
}
.v-table-row.unsatisfied-customer {
font-weight: bold;
}
.v-table-cell-content.premium-grade {
background-color: red;
}
.v-table-cell-content.high-grade {
background-color: green;
}
.v-table-cell-content.medium-grade {
background-color: blue;
}
Метод addPrintable() позволяет задать
специфическое представление данных колонки при
выводе в XLS-файл, осуществляемом стандартным
действием excel или напрямую с помощью класса
ExcelExporter. Метод принимает идентификатор
колонки и реализацию интерфейса Table.Printable
для нее. Например:
Метод getValue() интерфейса Table.Printable должен
возвращать данные, которые будут находиться в ячейке
таблицы. Это может быть не только строка - метод
может возвращать значения других типов, например,
числовые данные или даты, и они будут представлены в
XLS-файле соответствующим образом.
Если форматированный вывод в XLS необходим для
генерируемой колонки, нужно использовать
реализацию интерфейса
Table.PrintableColumnGenerator, передавая ее методу
addGeneratedColumn(). Значение для вывода в ячейку
XLS-документа задается в методе getValue() этого
интерфейса:
}
ordersTable.addPrintable("customer", new
Table.Printable<Customer, String>() {
@Override
public String getValue(Customer customer) {
return "Name: " + customer.getName;
}
});
ordersTable.addGeneratedColumn("product", new
Table.PrintableColumnGenerator<Order, String>() {
Если генерируемой колонке тем или иным способом не
задано представления Printable, то в случае, если
колонке соответствует атрибут сущности, будет
выведено его значение, в противном случае не будет
выведено ничего.
Метод setItemClickAction() позволяет задать
действие, выполняемое при двойном клике на строке
таблицы. Если такое действие не задано, при двойном
клике таблица пытается найти среди своих действий
подходящее в следующем порядке:
Действие, назначенное на клавишу EnterEnter посредством
свойства shortcut.
Действие с именем edit.
Действие с именем view.
Если такое действие найдено и имеет свойство enabled
= true, оно выполняется.
Table.PrintableColumnGenerator<Order, String>() {
@Override
public Component generateCell(Order entity) {
Label label =
componentsFactory.createComponent(Label.NAME);
Product product = order.getProduct();
label.setValue(product.getName() + ", " +
product.getCost());
return label;
}
@Override
public String getValue(Order entity) {
Product product = order.getProduct();
return product.getName() + ", " +
product.getCost();
}
});
= true, оно выполняется.
Метод setEnterPressAction() позволяет задать
действие, выполняемое при нажатии клавиши EnterEnter.
Если такое действие не задано, таблица пытается найти
среди своих действий подходящее в следующем
порядке:
Действие, назначенное методом setItemClickAction().
Действие, назначенное на клавишу EnterEnter посредством
свойства shortcut.
Действие с именем edit.
Действие с именем view.
Если такое действие найдено и имеет свойство enabled
= true, оно выполняется.
Атрибуты table table
align - aggregatable - aggregationStyle - columnControlVisible -
columnHeaderVisible - contextMenuEnabled - editable - enable -
height - id - multiLineCells - multiselect - presentations -
reorderingAllowed - settingsEnabled - showSelection - sortable -
stylename - tabIndex - textSelectionEnabled - visible - width
Элементы table table
actions - buttonsPanel - columns - rows - rowsCount
Атрибуты columncolumn
align - caption - captionProperty - collapsed - dateFormat - editable -
generator - id - link - linkInvoke - linkScreen - linkScreenOpenType -
maxTextLength - optionsDatasource - resolution - sortable - visible -
width
Элементы columncolumn
aggregation - formatter
Атрибуты aggregationaggregation
type - strategyClass
Атрибуты rowsrows
ДЕМОНСТРАЦИЯ
datasource
Предопределенные стили table table
borderless - compact - no-header - no-horizontal-lines - no-stripes -
no-vertical-lines - small
APIAPI
addGeneratedColumn - addPrintable - addColumnCollapseListener -
applySettings - generateCell - getSelected - requestFocus -
saveSettings - scrollTo - setClickListener - setEnterPressAction -
setItemClickAction - setStyleProvider
5.5.2.1.39. TextArea5.5.2.1.39. TextArea
Текстовая область − многострочное
текстовое поле для редактирования текста.
XML-имя компонента: textArea
Компонент TextArea реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
TextArea в основном повторяет функциональность
TextField и имеет следующие специфические атрибуты:
cols и rows задают количество строк и столбцов текста:
Значения width и height имеют приоритет над
значениями cols и rows.
resizableDirection – задаёт возможность изменения
размера области и его направление.
<textArea id="textArea" cols="20" rows="5"
caption="msg://name"/>
<textArea id="textArea" resizableDirection="BOTH"/>
Доступны следующие режимы изменения размера:
BOTH - компонент может изменять размер в обоих
направлениях. Режим не будет работать, если задан
размер компонента в процентах.
NONE - компонент не может изменять размер.
VERTICAL - компонент может изменять размер только по
вертикали. Режим не будет работать, если задана
высота компонента в процентах.
HORIZONTAL - компонент может изменять размер только
по горизонтали. Режим не будет работать, если задана
ширина компонента в процентах.
События изменения размеров области можно
отслеживать с помощью слушателя ResizeListener,
например:
wordwrap - установите данный атрибут в false, чтобы
отключить перенос строк по словам.
Компонент TextArea поддерживает слушатель
TextChangeListener, определёный в родительском
интерфейсе TextInputField. События изменения текста
обрабатываются асинхронно после ввода, не блокируя сам
ввод.
textArea.addResizeListener(e ->
showNotification("Resized"));
textArea.addTextChangeListener(event -> {
Параметром TextChangeEventMode задаётся режим
передачи изменений на сервер для вызова события на
серверной стороне. В платформе реализовано 3 режима
передачи:
LAZY (по умолчанию) - событие вызывается во время
паузы в наборе текста. Продолжительность паузы
можно задать с помощью метода
setInputEventTimeout(). Событие изменения текста
обрабатывается принудительно перед возможным
событием ValueChangeEvent, даже если пользователь не
выдержал паузу в наборе текста.
TIMEOUT - событие вызывается после периода ожидания.
В случае ввода нескольких изменений за один период,
на сервер отсылается событие со всеми изменениями,
включая последнее. Продолжительность периода
ожидания можно задать с помощью метода
setInputEventTimeout().
В случае, если ValueChangeEvent может случиться до
истечения периода ожидания, событие TextChangeEvent
обрабатывается до его истечения, при условии, что
набранный текст был изменён после предыдущего
TextChangeEvent.
EAGER - событие вызывается незамедлительно после
textArea.addTextChangeListener(event -> {
int length = event.getText().length();
textAreaLabel.setValue(length + " of " +
textArea.getMaxLength());
});
EAGER - событие вызывается незамедлительно после
каждого изменения текста, то есть после каждого
нажатия клавиш. Запросы отправляются по
отдельности и обрабатываются последовательно один
за другим. Тем не менее, асинхронная передача событий
изменения на сервер позволяет не блокировать
дальнейший ввод текста.
Стили компонента TextArea TextArea
В веб-клиенте с темой, основанной на Halo, к
компоненту TextArea можно применить
предопределенные стили. Стили задаются в XML-
дексрипторе или контроллере экрана с помощью
атрибута stylename:
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
TEXTAREA_:
align-center - выравнивание текста по центру
области.
align-right - выравнивание текста по правому краю
области.
borderless - удаляет рамку и фон текстовой области.
Атрибуты textArea textArea
align - caption - caseConversion - cols - datasource - datatype -
description - editable - enable - height - icon - id - maxLength -
property - required - requiredMessage - resizableDirection - rows -
<textArea id="textArea"
stylename="borderless"/>
textArea.setStyleName(HaloTheme.TEXTAREA_BORDERLESS
);
ДЕМОНСТРАЦИЯ
property - required - requiredMessage - resizableDirection - rows -
settingsEnabled - stylename - tabIndex - trim - visible - width -
wordwrap
Предопределенные стили textArea textArea
align-center - align-right - borderless - huge - large - small - tiny
APIAPI
addResizeListener - addTextChangeListener -
addValueChangeListener - applySettings - commit - discard -
isModied - saveSettings
5.5.2.1.40. TextField5.5.2.1.40. TextField
Поле для редактирования текста.
Может использоваться как для работы с атрибутами
сущностей, так и для ввода и отображения произвольной
текстовой информации.
XML-имя компонента: textField
Компонент текстового поля реализован для блоков WebWeb
ClientClient и Desktop ClientDesktop Client.
Пример текстового поля с заголовком, взятым из пакета
локализованных сообщений:
На рисунке ниже показан вид простого текстового поля.
В веб-клиенте с темой, основанной на Halo, к компоненту
TextField можно применить предопределенные стили.
Стили задаются в XML-дексрипторе или контроллере
экрана с помощью атрибута stylename:
<textField id="nameField" caption="msg://name"/>
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
TEXTFIELD_:
Стили компонента TextField:
align-center - выравние текста по центру поля.
align-right - выравнивание текста по правому краю
поля.
borderless - удаляет рамку и фон текстового поля.
inline-icon - расположение пиктограммы внутри
текстового поля.
Компонент TextField поддерживает автоматическую
конвертацию регистра. Атрибут caseConvertion может
принимать следующие значения:
UPPER - верхний регистр,
LOWER - нижний регистр,
NONE - конвертация отключена (значение по
умолчанию). Используйте это значение для поддержки
клавиатурного ввода с использованием IME, к примеру,
для японского, корейского и китайского языков.
Для создания текстового поля, связанного с данными,
необходимо использовать атрибуты datasource и property.
<textField id="textField"
stylename="borderless"/>
textField.setStyleName(HaloTheme.TEXTFIELD_INLINE_IC
ON);
<dsContext>
<datasource id="customerDs"
class="com.sample.sales.entity.Customer"
Как видно из примера, в экране описывается источник
данных customerDs для некоторой сущности
Покупатель (Customer), имеющей атрибут name. В
компоненте текстового поля в атрибуте datasource
указывается ссылка на источник данных, а в атрибуте
property − название атрибута сущности, значение
которого должно быть отображено в текстовом поле.
Если поле не связано с атрибутом сущности (то есть не
указан источник данных и название атрибута), то можно
указать тип данных с помощью атрибута datatype. Тип
данных используется для форматирования значения
поля. В качестве значения атрибута может быть указано
любое имя типа данных, зарегистрированного в
метаданных приложения - см. Datatype. Как правило, в
TextField используются следующие типы данных:
decimal
double
int
long
В качестве примера рассмотрим текстовое поле с типом
данных Integer.
Если в таком поле ввести значение, которое
class="com.sample.sales.entity.Customer"
view="_local"/>
</dsContext>
<layout>
<textField datasource="customerDs"
property="name" caption="msg://name"/>
<textField id="integerField" datatype="int"
caption="msg://integerFieldName"/>
Если в таком поле ввести значение, которое
невозможно интерпретировать как целое число, то при
потере фокуса полем будет выведено сообщение об
ошибке и значение поля вернется на предыдущее.
Текстовому полю может быть назначен валидатор -
класс, реализующий интерфейс Field.Validator.
Валидатор позволяет дополнительно к datatype
ограничить вводимую пользователем информацию.
Например, для создания поля ввода положительных
целых чисел нужно создать класс валидатора:
и задать его для текстового поля с типом данных int в
элементе validator:
В отличие от проверки вводимой строки на
соответствие типу данных, валидация срабатывает не
сразу при потере полем фокуса, а только при вызове у
public class PositiveIntegerValidator implements
Field.Validator {
@Override
public void validate(Object value) throws
ValidationException {
Integer i = (Integer) value;
if (i <= 0)
throw new ValidationException("Value
must be positive");
}
}
<textField id="integerField" datatype="int">
<validator
class="com.sample.sales.gui.PositiveIntegerValidator
"/>
</textField>
сразу при потере полем фокуса, а только при вызове у
поля метода validate(). Это означает, что поле (и
связанный с ним атрибут сущности) может некоторое
время содержать значение, не удовлетворяющее
условиям валидации (в приведенном примере
неположительное число). Это не является проблемой,
так как обычно поля редактирования с валидацией
располагаются в экране редактирования, а он
автоматически вызывает валидацию всех своих полей
перед коммитом. Если же поле находится не в экране
редактирования, то необходимо вызывать метод
validate() поля в контроллере явно.
Компонент TextField поддерживает слушатель
TextChangeListener, определёный в родительском
интерфейсе TextInputField. События изменения текста
обрабатываются асинхронно после ввода, не блокируя
сам ввод.
Параметром TextChangeEventMode задаётся режим
передачи изменений на сервер для вызова события на
серверной стороне. В платформе реализовано 3 режима
передачи:
LAZY (по умолчанию) - событие вызывается во время
textField.addTextChangeListener(event -> {
int length = event.getText().length();
textFieldLabel.setValue(length + " of " +
textField.getMaxLength());
});
textField.setTextChangeEventMode(TextInputField.Text
ChangeEventMode.LAZY);
паузы в наборе текста. Продолжительность паузы
можно задать с помощью метода
setInputEventTimeout(). Событие изменения текста
обрабатывается принудительно перед возможным
событием ValueChangeEvent, даже если пользователь
не выдержал паузу в наборе текста.
TIMEOUT - событие вызывается после периода
ожидания. В случае ввода нескольких изменений за
один период, на сервер отсылается событие со всеми
изменениями, включая последнее. Продолжительность
периода ожидания можно задать с помощью метода
setInputEventTimeout().
В случае, если ValueChangeEvent может случиться до
истечения периода ожидания, событие
TextChangeEvent обрабатывается до его истечения, при
условии, что набранный текст был изменён после
предыдущего TextChangeEvent.
EAGER - событие вызывается незамедлительно после
каждого изменения текста, то есть после каждого
нажатия клавиш. Запросы отправляются по
отдельности и обрабатываются последовательно один
за другим. Тем не менее, асинхронная передача
событий изменения на сервер позволяет не
блокировать дальнейший ввод текста.
EnterPressListener позволяет указать действие,
которое должно быть выполнено по нажатию клавиши
Enter:
ValueChangeListener позволяет обрабатывать
изменения значения в текстовом поле, когда
textField.addEnterPressListener(e ->
showNotification("Enter pressed"));
изменения значения в текстовом поле, когда
пользователь уже закончил ввод, т.е. после нажатия
клавиши Enter или при потере компонентом фокуса. В
слушатель передается объект события типа
ValueChangeEvent, который имеет следующие методы:
getPrevValue() возвращает значение компонента до
изменения.
getValue() возвращает текущее значение компонента.
Если текстовое поле связано с атрибутом сущности
(через datasource и property), и если для атрибута
сущности в JPA-аннотации @Column указан параметр
length, то TextField будет соответственно
ограничивать максимальную длину вводимого текста.
Если текстовое поле не связано с атрибутом, либо для
него не определено значение length, либо это значение
нужно переопределить, то для ограничения
максимальной длины вводимого текста можно
использовать атрибут maxLength. Значение "-1" означает
отсутствие ограничения. Например:
По умолчанию текстовое поле отсекает пробелы в
начале и конце введенной строки. То есть если
пользователь ввел строку " aaa bbb " то значением поля,
возвращаемым методом getValue() и сохраняемым в
связанный атрибут сущности, будет строка "aaa bbb". Для
того, чтобы отключить отсечение пробелов, используйте
атрибут trim со значением false.
textField.addValueChangeListener(e ->
showNotification("Before: " +
e.getPrevValue() + ". After: " + e.getValue()));
<textField id="shortTextField" maxLength="10"/>
ДЕМОНСТРАЦИЯ
атрибут trim со значением false.
Следует иметь в виду, что отсечение пробелов работает
только при вводе нового значения. Если в значении
связанного атрибута уже присутствуют пробелы, они
будут отображаться, пока пользователь не изменит
значение поля.
Текстовое поле всегда вместо введенной пустой строки
возвращает null. Соответственно, при включенном
атрибуте trim строка, состоящая из одних пробелов
также превратится в null.
Метод setCursorPosition() используется для
установки позиции курсора в указанный индекс
(начинается с 0). После вызова метода поле принимает
фокус ввода.
Атрибуты textField textField
align - caption - caseConversion - datasource - datatype -
description - editable - enable - height - icon - id - inputPrompt -
maxLength - property - required - requiredMessage - stylename -
tabIndex - trim - visible - width
Элементы textField textField
validator
Предопределенные стили textField textField
align-center - align-right - borderless - huge - inline-icon - large -
small - tiny
APIAPI
addEnterPressListener - addTextChangeListener -
addValueChangeListener - commit - discard - isModied
5.5.2.1.41. TimeField5.5.2.1.41. TimeField
Поле для отображения и ввода
времени.
XML-имя компонента: timeField.
Компонент TimeField реализован для блоков Web ClientWeb Client и
Desktop ClientDesktop Client.
Для создания поля времени, связанного с данными,
необходимо использовать атрибуты datasource и property:
Как видно из примера, в экране описывается источник
данных orderDs для некоторой сущности Заказ (Order),
имеющей атрибут deliveryTime. В компоненте ввода
времени в атрибуте datasource указывается ссылка на
источник данных, а в атрибуте property − название
атрибута сущности, значение которого должно быть
отображено в поле.
Связанный атрибут сущности должен быть типа
java.util.Date или java.sql.Time.
Формат отображения времени определяется типом
данных time и задается в главном пакете
локализованных сообщений в ключе timeFormat.
Формат отображения времени можно также задать в
атрибуте timeFormat компонента. Это может быть как
сама строка формата, так и ключ в пакете сообщений (с
префиксом msg://).
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="_local"/>
</dsContext>
<layout>
<timeField datasource="orderDs"
property="deliveryTime"/>
ДЕМОНСТРАЦИЯ
префиксом msg://).
Независимо от упомянутого выше формата
отображением секунд можно управлять с помощью
атрибута showSeconds. По умолчанию секунды
отображаются, если формат содержит символы ss.
Атрибуты timeField timeField
align - caption - editable - enable - datasource - description - height -
icon - id - property - required - requiredMessage - showSeconds -
stylename - tabIndex - timeFormat - visible - width
Элементы timeField timeField
validator
APIAPI
addValueChangeListener - commit - discard - isModied
5.5.2.1.42. TokenList5.5.2.1.42. TokenList
Компонент TokenList представляет
собой упрощенный вариант работы со списком
сущностей: названия экземпляров располагаются в
вертикальном или горизонтальном списке, добавление
производится из выпадающего списка, удаление - с
помощью кнопок, расположенных рядом с каждым
экземпляром.
<timeField datasource="orderDs" property="createTs"
showSeconds="true"/>
XML-имя компонента: tokenList
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Пример описания компонента TokenList в XML-
дескрипторе экрана:
Здесь в элементе dsContext определен вложенный
источник данных productsDs, содержащий коллекцию
входящих в состав заказа продуктов. Кроме того,
определен источник данных allProductsDs, содержащий
коллекцию всех продуктов, имеющихся в базе данных.
Компонент TokenList с идентификатором productsList
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="order-edit">
<collectionDatasource id="productsDs"
property="products"/>
</datasource>
<collectionDatasource id="allProductsDs"
class="com.sample.sales.entity.Product"
view="_minimal">
<query>select p from sales$Product p order by
p.name</query>
</collectionDatasource>
</dsContext>
<layout>
<tokenList id="productsList"
datasource="productsDs" inline="true" width="500px">
<lookup optionsDatasource="allProductsDs"/>
</tokenList>
отображает содержимое источника данных productsDs, а
также позволяет изменять эту коллекцию, добавляя в
него экземпляры из источника данных allProductsDs.
Атрибуты tokenList:
position - задает позиционирование раскрывающегося
списка. Атрибут может принимать два значения: TOP,
BOTTOM. По умолчанию TOP.
Атрибут inline задает отображение списка выбранных
значений: вертикально или горизонтально. Значение
true соответствует горизонтальному расположению,
значение false − вертикальному. Так выглядит
компонент с горизонтальным расположением значений:
simple - значение true позволяет убрать компонент
выбора, оставляя только кнопку добавления и очистки
списка. При нажатии на кнопку добавления AddAdd сразу
показывается экран списка экземпляров сущности, тип
которой задан источником данных datasource.
Идентификатор экрана выбора определяется по
правилам, описанным для стандартного действия
PickerField.LookupAction. Кнопка очистки списка ClearClear
удаляет все элементы из источника данных компонента
TokenList.
clearEnabled - значение false позволяет скрыть кнопку
очистки ClearClear.
Элементы tokenList:
lookup − описатель компонента выбора значений.
Атрибуты элемента lookup:
Атрибут lookup задает возможность выбора значений
через экран выбора сущностей:
inputPrompt - текстовая подсказка, которая
отображается в поле выбора. Если подсказка не задана,
поле будет пустым.
Атрибут lookupScreen задает идентификатор экрана
для выбора значений в режиме lookup="true". Если
данный атрибут не задан, то идентификатор экрана
выбора определяется по правилам, описанным для
стандартного действия PickerField.LookupAction.
Атрибут openType можно задать способ открытия
<tokenList id="linesList"
datasource="orderItemsDs"
width="320px">
<lookup optionsDatasource="allItemsDs"
inputPrompt="Choose an item" />
</tokenList>
Атрибут openType можно задать способ открытия
экрана выбора, аналогично описанному для
стандартного действия PickerField.LookupAction. По
умолчанию - THIS_TAB.
Если значение атрибута multiselect установлено в
true, то в мэп параметров экрана выбора в ключе
MULTI_SELECT передается значение true. Этот признак
можно использовать для установки в экране режима
множественного выбора. Данный ключ определен в
перечислении WindowParams, поэтому с ним удобно
работать следующим образом:
addButton − описатель кнопки добавления значений.
Может содержать атрибуты caption и icon.
Слушатели tokenList:
ItemClickListener позволяет отслеживать клики по
элементам tokenList.
ValueChangeListener отслеживает изменения
значенияtokenList, так же, как и любого другого
компонента, реализующего интерфейс Field.
Атрибуты tokenList tokenList
align - caption - captionProperty - clearEnabled - datasource -
description - editable - enable - height - icon - id - inline - position -
simple - stylename - tabIndex - visible - width
Элементы tokenList tokenList
@Override
public void init(Map<String, Object> params) {
if
(WindowParams.MULTI_SELECT.getBool(getContext())) {
usersTable.setMultiSelect(true);
}
}
ДЕМОНСТРАЦИЯ
Элементы tokenList tokenList
addButton - lookup
Атрибуты lookuplookup
captionProperty - lterMode - inputPrompt - lookup - lookupScreen -
multiselect - openType - optionsDatasource
Атрибуты buttonbutton
caption - icon
APIAPI
addValueChangeListener - setItemClickListener
5.5.2.1.43. Tree5.5.2.1.43. Tree
Компонент Tree предназначен для
отображения иерархической структуры, представленной
сущностями, содержащими ссылки на самих себя.
XML-имя компонента: tree
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Для Tree в атрибуте datasource элемента treechildren
должен быть указан hierarchicalDatasource. Объявление
hierarchicalDatasource должно содержать атрибут
hierarchyProperty - имя атрибута сущности, являющегося
ссылкой на саму себя.
Пример описания компонента Tree в XML-дескрипторе
экрана:
<dsContext>
<hierarchicalDatasource id="departmentsDs"
class="com.sample.sales.entity.Department"
В атрибуте captionProperty элемента treechildren
можно задать имя свойства сущности, отображаемого в
дереве. Если этот атрибут не определен, то будет
отображаться имя экземпляра сущности.
Атрибут multiselect позволяет задать режим
множественного выделения элементов дерева. Если
multiselect равен true, то пользователь может выделить
несколько элементов с помощью клавиатуры или мыши,
удерживая клавиши CtrlCtrl или ShiftShift. По умолчанию режим
множественного выделения отключен.
Метод setItemClickAction() позволяет задать действие,
которое будет выполнено при двойном клике по узлу
дерева.
Каждый элемент дерева может иметь пиктограмму слева.
Создайте реализацию интерфейса
ListComponent.IconProvider в контроллере экрана и
установите ее для компонента Tree:
class="com.sample.sales.entity.Department"
view="browse"
hierarchyProperty="parentDept">
<query>
select d from sales$Department d order by
d.createTs
</query>
</hierarchicalDatasource>
</dsContext>
<layout>
<tree id="departmentsTree" width="100%"
height="100%">
<treechildren datasource="departmentsDs"
captionProperty="name"/>
</tree>
ДЕМОНСТРАЦИЯ
Атрибуты tree tree
enable - height - id - multiselect - stylename - tabIndex - visible -
width
Элементы tree tree
actions - treechildren
Атрибуты treechildren treechildren
captionProperty - datasource
5.5.2.1.44. TreeTable5.5.2.1.44. TreeTable
Компонент TreeTable − иерархическая
таблица, отображающая в первой колонке древовидную
структуру. Предназначена для работы с сущностями,
которые содержат ссылки на самих себя. Это могут быть
например, файловая система или организационная
структура предприятия.
@Inject
private Tree<Region> tree;
@Override
public void init(Map<String, Object> params) {
tree.setIconProvider(new
ListComponent.IconProvider<Region>() {
@Nullable
@Override
public String getItemIcon(Region entity) {
if (entity.getParent() == null) {
return "icons/root.png";
}
return "icons/leaf.png";
}
});
}
структура предприятия.
XML-имя компонента: treeTable
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient.
Для TreeTable в атрибуте datasource элемента rows
должен быть указан hierarchicalDatasource. Объявление
hierarchicalDatasource должно содержать атрибут
hierarchyProperty - имя атрибута сущности, являющегося
ссылкой на саму себя.
Пример описания таблицы в XML-дескрипторе экрана:
<dsContext>
<hierarchicalDatasource id="tasksDs"
class="com.sample.sales.entity.Task" view="browse"
hierarchyProperty="parentTask">
<query>
select t from sales$Task t
</query>
</hierarchicalDatasource>
</dsContext>
<layout>
<treeTable id="tasksTable" width="100%">
<columns>
<column id="name"/>
<column id="dueDate"/>
<column id="assignee"/>
</columns>
ДЕМОНСТРАЦИЯ
Функциональность TreeTable аналогична простой
таблице Table.
Атрибуты treeTable treeTable
align - aggregatable - aggregationStyle - columnControlVisible -
contextMenuEnabled - editable - enable - height - id - multiLineCells
- multiselect - presentations - reorderingAllowed - settingsEnabled -
sortable - stylename - tabIndex - textSelectionEnabled - visible -
width
Элементы treeTable treeTable
actions - buttonsPanel - columns - rows - rowsCount
Атрибуты columncolumn
align - caption - captionProperty - collapsed - dateFormat - editable -
id - link - linkInvoke - linkScreen - linkScreenOpenType -
maxTextLength - optionsDatasource - resolution - sortable - visible -
width
Элементы columncolumn
aggregation - formatter
Атрибуты rowsrows
datasource
APIAPI
addColumnCollapseListener - setClickListener
5.5.2.1.45. TwinColumn5.5.2.1.45. TwinColumn
Компонент TwinColumn представляет
собой сдвоенный список для множественного выбора
опций. В левом списке содержатся доступные
невыбранные значения, в правом списке содержатся
выбранные значения. Пользователь выбирает значения,
</columns>
<rows datasource="tasksDs"/>
</treeTable>
выбранные значения. Пользователь выбирает значения,
перенося их из левого в правый список и обратно с
помощью двойного клика или соответствующих кнопок.
Для каждого значения можно задать уникальный стиль
отображения и пиктограмму.
XML-имя компонента: twinColumn
Компонент реализован только для блока Web ClientWeb Client.
Пример использования компонента twinColumn для
выбора экземпляров сущности:
В данном случае компонент coloursField отобразит
имена экземпляров сущности Colour, находящихся в
источнике данных coloursDs, а его метод getValue()
вернет коллекцию выбранных экземпляров сущности.
Атрибут addAllBtnEnabled задает отображение кнопок,
позволяющих перемещать между списками все опции
<dsContext>
<datasource id="carDs"
class="com.company.sample.entity.Car" view="_local"/>
<collectionDatasource id="coloursDs"
class="com.company.sample.entity.Colour"
view="_local">
<query>select c from sample$Colour c</query>
</collectionDatasource>
</dsContext>
<layout>
<twinColumn id="coloursField"
optionsDatasource="coloursDs"
addAllBtnEnabled="true"/>
позволяющих перемещать между списками все опции
сразу.
Атрибут columns используется для задания количества
символов в строке, а атрибут rows − для задания
количества строк текста в каждом списке.
Атрибуты leftColumnCaption и rightColumnCaption
используются для назначения заголовков списков.
Для задания внешнего вида опций можно реализовать
интерфейс TwinColumn.StyleProvider и возвращать
название стиля и путь к пиктограмме в зависимости от
конкретного экземпляра сущности, отображаемого в
компоненте.
Список опций компонента TwinColumn может быть задан
произвольно с помощью методов setOptionsList(),
setOptionsMap() и setOptionsEnum(), аналогично
описанному для компонента OptionsGroup.
Атрибуты twinColumn twinColumn
align - addAllBtnEnabled - caption - captionProperty - columns -
datasource - description - editable - enable - height - icon - id -
leftColumnCaption - optionsDatasource - property - required -
requiredMessage - rightColumnCaption - rows - stylename -
tabIndex - visible - width
Элементы twinColumn twinColumn
validator
APIAPI
addValueChangeListener
5.5.2.2. 5.5.2.2. Контейнеры
Accordion
BoxLayout
ДЕМОНСТРАЦИЯ
BoxLayout
ButtonsPanel
CssLayout
Frame
GridLayout
GroupBoxLayout
HtmlBoxLayout
ScrollBoxLayout
SplitPanel
TabSheet
5.5.2.2.1. Accordion5.5.2.2.1. Accordion
Контейнер Accordion - это
вертикальный контейнер со сворачиваемыми вкладками,
который позволяет легко скрывать и отображать
большой объем контента. Accordion реализован для
блока Web ClientWeb Client.
XML-имя компонента: accordion. Пример описания
аккордеона в XML-дескрипторе экрана:
<accordion id="accordion" height="100%">
<tab id="tabStamford" caption="msg://tabStamford"
margin="true" spacing="true">
Компонент accordion должен иметь вложенные
элементы tab, описывающие вкладки. Каждая вкладка
является контейнером с вертикальным расположением
компонентов, аналогичным vbox. Контейнер аккордеон
может быть использован при нехватке места на странице
приложения, или же если название вкладки слишком
длинное для отображения в TabSheet. Аккордеон
предоставляет анимацию плавного перехода.
Атрибуты элемента tab:
id – идентификатор вкладки. Следует отметить, что
вкладка не является компонентом, и данный
идентификатор используется только в рамках
Accordion для работы с ней из кода контроллера..
caption – заголовок вкладки.
icon - указывает на местоположение пиктограммы в
каталоге темы. Подробную информацию о том, где
следует располагать файлы пиктограмм, можно
прочитать в Темы приложения.
lazy – задает отложенную загрузку содержимого
вкладки.
При открытии экрана lazy-вкладки не загружают свое
margin="true" spacing="true">
<label value="msg://sampleStamford"/>
</tab>
<tab id="tabBoston" caption="msg://tabBoston"
margin="true" spacing="true">
<label value="msg://sampleBoston"/>
</tab>
<tab id="tabLondon" caption="msg://tabLondon"
margin="true" spacing="true">
<label value="msg://sampleLondon"/>
</tab>
</accordion>
При открытии экрана lazy-вкладки не загружают свое
содержимое, что приводит к созданию меньшего
количества компонентов в памяти. Компоненты
вкладки загружаются только в тот момент, когда
пользователь выбирает данную вкладку. Кроме того,
если на lazy-вкладке расположены визуальные
компоненты, связанные с источником данных,
содержащим JPQL запрос, то этот запрос также не
выполняется. В результате экран открывается быстрее,
а данные загружаются только в тот момент, когда
пользователь действительно хочет их увидеть, выбирая
данную вкладку.
Следует иметь в виду, что компоненты, расположенные
на lazy-вкладке, не существуют в момент открытия
экрана. Поэтому их нельзя инжектировать в
контроллер, и нельзя получить вызовом getComponent()
в методе init() контроллера. Обратиться к
компонентам lazy-вкладки можно только после того,
как пользователь на нее переключился. Этот момент
можно отловить с помощью слушателя
Accordion.SelectedTabChangeListener, например:
@Inject
private Accordion accordion;
private boolean tabInitialized;
@Override
public void init(Map<String, Object> params) {
accordion.addSelectedTabChangeListener(event ->
{
if
("tabCambridge".equals(event.getSelectedTab().getNam
e())) {
initCambridgeTab();
По умолчанию вкладки не являются lazy, а значит,
загружают свое содержимое в момент открытия экрана.
В веб-клиенте с темой, основанной на Halo, атрибут
stylename позволяет установить для компонента
accordion стиль borderless, который удаляет рамку и
фон контейнера:
Вкладка компонента accordion может содержать любой
другой визуальный контейнер, такой как таблица, сетка и
т.д.:
initCambridgeTab();
}
});
}
private void initCambridgeTab(){
if (tabInitialized) {
return;
}
tabInitialized = true;
// initialization code here
// use getComponentNN("comp_id") here to get
lazy tab's components
}
accordion.setStyleName(HaloTheme.ACCORDION_BORDERLES
S);
<accordion id="accordion" height="100%" width="100%"
enable="true">
<tab id="tabNY" caption="msg://tabNY"
margin="true" spacing="true">
<table id="nYTable" width="100%">
<columns>
Атрибуты accordion accordion
colspan - enable - height - id - rowspan - stylename -
tabCaptionsAsHtml - tabIndex - visible - width
Атрибуты tab tab
caption - description - enable - expand - icon - id - lazy - margin -
spacing - stylename - visible
APIAPI
add - addSelectedTabChangeListener - getComponent -
getComponentNN - getComponents - getOwnComponent -
getOwnComponents - remove - removeAll
5.5.2.2.2. BoxLayout5.5.2.2.2. BoxLayout
BoxLayout представляет собой контейнер с
<columns>
<column id="borough"/>
<column id="county"/>
<column id="population"/>
<column id="square"/>
</columns>
<rows datasource="newYorkDs"/>
</table>
</tab>
</accordion>
ДЕМОНСТРАЦИЯ
ДЕМОНСТРАЦИЯ
BoxLayout представляет собой контейнер с
последовательным размещением компонентов.
Существует три типа BoxLayout, определяемых именем
XML-элемента:
hbox − горизонтальное расположение
компонентов.
vbox − вертикальное расположение
компонентов. vbox имеет 100% ширину по умолчанию.
<hbox spacing="true" margin="true">
<dateField datasource="orderDs"
property="date"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs"/>
<textField datasource="orderDs"
property="amount"/>
</hbox>
<vbox spacing="true" margin="true">
<dateField datasource="orderDs"
property="date"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs"/>
ДЕМОНСТРАЦИЯ
flowBox − горизонтальное
расположение компонентов с переносом вниз. При
недостатке места по горизонтали непомещающиеся
компоненты будут перенесены "на следующую строку"
(поведение аналогично SwingSwing FlowLayout).
В веб-клиенте с темой, основанной на Halo, BoxLayout
может быть использован для создания сложных
составных компонентов. Атрибут stylename со значением
card или well в сочетании с атрибутом stylename="v-
panel-caption" вложенного контейнера задают
компоненту внешний вид Vaadin Panel.
стиль card придаёт контейнеру вид карточки.
well делает карточку "утопленной" с затемнением
фона.
optionsDatasource="customersDs"/>
<textField datasource="orderDs"
property="amount"/>
</vbox>
<flowBox spacing="true" margin="true">
<dateField datasource="orderDs"
property="date"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs"/>
<textField datasource="orderDs"
property="amount"/>
</flowBox>
Метод getComponent() позволяет получить дочерний
компонент BoxLayout по его индексу:
<vbox stylename="well"
height="200px"
width="300px"
expand="message"
spacing="true">
<hbox stylename="v-panel-caption"
width="100%">
<label value="Widget caption"/>
<button align="MIDDLE_RIGHT"
icon="font-icon:EXPAND"
stylename="borderless-colored"/>
</hbox>
<textArea id="message"
inputPrompt="Enter your message here..."
width="280"
align="MIDDLE_CENTER"/>
<button caption="Send message"
width="100%"/>
</vbox>
Button button = (Button) hbox.getComponent(0);
ДЕМОНСТРАЦИЯ
В компоненте BoxLayout можно использовать горячие
клавиши. Задать сочетание клавиш и вызываемое
действие можно с помощью метода addShortcutAction():
Атрибуты hbox, vbox, flowBox hbox, vbox, flowBox
align - enable - expand - height - id - margin - spacing - stylename -
visible - width
APIAPI
add - addLayoutClickListener - addShortcutAction - getComponent -
getComponentNN - getComponents - getMargin -
getOwnComponent - getOwnComponents - indexOf - remove -
removeAll - setMargin - setSpacing
5.5.2.2.3. ButtonsPanel5.5.2.2.3. ButtonsPanel
ButtonsPanel - контейнер,
унифицирующий использование и размещение
компонентов (чаще всего кнопок) для управления
данными в таблице.
XML-имя компонента: buttonsPanel.
Пример описания ButtonsPanel в XML-дескрипторе
экрана:
flowBox.addShortcutAction(new ShortcutAction("SHIFT-
A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
<table id="customersTable"
editable="false" width="100%">
Элемент buttonsPanel можно разместить как внутри
table, так и в произвольном месте экрана.
Если buttonsPanel находится внутри table, то она
комбинируется с компонентом rowsCount таблицы, тем
самым оптимально расходуя место по вертикали. Кроме
того, в этом случае при открытии экрана выбора методом
Frame.openLookup() (например, из компонента PickerField)
панель кнопок скрывается.
Атрибут alwaysVisible служит для отключения скрытия
панели в экране выбора при его открытии методом
Frame.openLookup(). Если значение атрибута равно true,
то панель с кнопками не скрывается. По умолчанию
значение атрибута равно false.
События щелчка по области компонента buttonsPanel
можно отслеживать с помощью интерфейса
editable="false" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
<action id="excel"/>
</actions>
<buttonsPanel>
<button action="customersTable.create"/>
<button action="customersTable.edit"/>
<button action="customersTable.remove"/>
<button action="customersTable.excel"/>
</buttonsPanel>
<columns>
<column id="name"/>
<column id="email"/>
</columns>
<rows datasource="customersDs"/>
</table>
ДЕМОНСТРАЦИЯ
можно отслеживать с помощью интерфейса
LayoutClickListener.
В компоненте ButtonsPanel можно использовать горячие
клавиши. Задать сочетание клавиш и вызываемое
действие можно с помощью метода addShortcutAction():
Атрибуты buttonsPanel buttonsPanel
align - alwaysVisible - enable - expand - height - id - stylename -
visible - width
APIAPI
addLayoutClickListener - addShortcutAction - setSpacing
5.5.2.2.4. CssLayout5.5.2.2.4. CssLayout
Контейнер CssLayout позволяет
управлять размещением и стилизацией своих
компонентов с помощью CSS.
XML-имя компонента: cssLayout.
Ниже приведен пример использования cssLayout в
простом responsive экране.
Отображение компонентов на широком дисплее:
buttonsPanel.addShortcutAction(new
ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Отображение компонентов на узком дисплее:
XML-дескриптор экрана:
<cssLayout responsive="true"
stylename="responsive-container"
width="100%">
<vbox margin="true"
spacing="true"
stylename="group-panel">
<textField caption="Field One" width="100%"/>
<textField caption="Field Two" width="100%"/>
<button caption="Button"/>
</vbox>
<vbox margin="true"
spacing="true"
stylename="group-panel">
Содержимое файла modules/web/themes/halo/halo-
ext.scss (в разделе Расширение существующей темы
приведена информация о том как создать этот файл):
Атрибут stylename позволяет применять стили к
компоненту CssLayout в XML-дескрипторе или
контроллере экрана.
stylename="group-panel">
<textField caption="Field Three"
width="100%"/>
<textField caption="Field Four" width="100%"/>
<button caption="Button"/>
</vbox>
</cssLayout>
/* Define your theme modifications inside next mixin
*/
@mixin halo-ext {
@include halo;
.responsive-container {
&[width-range~="0-900px"] {
.group-panel {
width: 100% !important;
}
}
&[width-range~="901px-"] {
.group-panel {
width: 50% !important;
}
}
}
}
стиль v-component-group используется для склеивания
компонентов, т.е. группировки без отступов между
ними:
стиль well делает контейнер "утопленным" с
затемнением фона.
стиль card придаёт контейнеру вид карточки. В
сочетании со стилем v-panel-caption, установленным
для любого вложенного контейнера, он позволяет
создавать сложные составные контейнеры, например:
Результат:
<cssLayout stylename="v-component-group">
<textField inputPrompt="Search..."/>
<button caption="OK"/>
</cssLayout>
<cssLayout height="300px"
stylename="card"
width="300px">
<hbox stylename="v-panel-caption"
width="100%">
<label value="Widget caption"/>
<button align="MIDDLE_RIGHT"
icon="font-icon:EXPAND"
stylename="borderless-colored"/>
</hbox>
<vbox height="100%">
<label value="Panel content"/>
</vbox>
</cssLayout>
ДЕМОНСТРАЦИЯ
Результат:
Атрибуты cssLayout cssLayout
enable - height - id - responsive - stylename - visible - width
APIAPI
add - addShortcutAction - addLayoutClickListener - getComponent -
getComponentNN - getComponents - getOwnComponent -
getOwnComponents - indexOf - remove - removeAll
5.5.2.2.5. Frame5.5.2.2.5. Frame
Элемент frame предназначен для
включения в экран фреймов.
Атрибуты:
src − путь к XML-дескриптору фрейма.
screen - идентификатор фрейма в screens.xml (если
фрейм зарегистрирован).
Должен быть указан один из этих атрибутов. Если указано
оба, фрейм будет загружен из явно указанного в src
файла.
Атрибуты frame frame
ДЕМОНСТРАЦИЯ
Атрибуты frame frame
align - height - id - screen - src - stylename - visible - width
APIAPI
add - getComponent - getComponentNN - getComponents -
getMargin - getOwnComponent - getOwnComponents - indexOf -
remove - removeAll - setMargin - setSpacing
5.5.2.2.6. GridLayout5.5.2.2.6. GridLayout
GridLayout - контейнер,
располагающий компоненты по сетке.
XML-имя компонента: grid.
Пример использования контейнера:
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Date" align="MIDDLE_LEFT"/>
<dateField datasource="orderDs"
property="date"/>
<label value="Customer"
align="MIDDLE_LEFT"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs"/>
</row>
<row>
<label value="Amount"
align="MIDDLE_LEFT"/>
<textField datasource="orderDs"
Элементы grid:
columns - обязательный элемент, описывает колонки
сетки. Должен либо иметь атрибут count, либо
вложенные элементы column для каждой колонки.
В простейшем случае достаточно задать число колонок
в атрибуте count. Тогда, если ширина всего контейнера
явно задана в пикселах или процентах, незанятое место
будет распределяться между колонками равными
долями.
Для распределения незанятого места неравными
долями необходимо определить для каждой колонки
элемент column и задать для него атрибут flex.
Пример сетки, в которой вторая и четвертая колонки
занимают все лишнее место по горизонтали, причем
четвертая колонка забирает себе в три раза больше
лишнего места:
<textField datasource="orderDs"
property="amount"/>
</row>
</rows>
</grid>
<grid spacing="true" width="100%">
<columns>
<column/>
<column flex="1"/>
<column/>
<column flex="3"/>
</columns>
<rows>
<row>
<label value="Date"/>
<dateField datasource="orderDs"
Если атрибут flex не указан, или указано значение 0, то
ширина данной колонки будет установлена по
содержимому, если хотя-бы одна другая колонка имеет
ненулевой flex. В приведенном примере первая и
третья колонки получат ширину по максимальной длине
текста надписей.
Для того, чтобы лишнее место вообще
образовалось, необходимо установить всему
контейнеру ширину в пикселах или процентах. В
противном случае ширина колонок будет
рассчитана по ширине содержимого, и атрибут flex
не будет иметь никакого эффекта.
rows − обязательный элемент, содержит
последовательность строк. Каждая строка
определяется в своем элементе row.
Элемент row может содержать атрибут flex,
аналогичный описанному для column, но влияющий на
распределение лишнего места по вертикали при
<dateField datasource="orderDs"
property="date" width="100%"/>
<label value="Customer"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs" width="100%"/>
</row>
<row>
<label value="Amount"/>
<textField datasource="orderDs"
property="amount" width="100%"/>
</row>
</rows>
</grid>
распределение лишнего места по вертикали при
заданной общей высоте сетки.
Элемент row должен содержать элементы компонентов,
отображаемых в ячейках данной строки сетки. Число
компонентов в одной строке не должно превышать
заданного количества колонок, но может быть меньше.
Любой компонент, находящийся в контейнере grid, может
иметь атрибуты colspan и rowspan. Эти атрибуты задают
соответственно сколько колонок и строк будет занимать
данный компонент. Например, так можно растянуть поле
Field3 на три колонки:
В результате компоненты будут располагаться
следующим образом:
События щелчка по области компонента GridLayout
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Name 1"/>
<textField/>
<label value="Name 2"/>
<textField/>
</row>
<row>
<label value="Name 3"/>
<textField colspan="3" width="100%"/>
</row>
</rows>
</grid>
События щелчка по области компонента GridLayout
можно отслеживать с помощью интерфейса
LayoutClickListener.
Метод getComponent() позволяет получить дочерний
компонент GridLayout по индексам его колонки и строки:
В компоненте GridLayout можно использовать горячие
клавиши. Задать сочетание клавиш и вызываемое
действие можно с помощью метода addShortcutAction():
Атрибуты grid grid
align - enable - height - id - margin - spacing - stylename - visible -
width
Элементы grid grid
columns - rows
Атрибуты columns columns
count
Атрибуты column column
ex
Атрибуты row row
ex - visible
APIAPI
add - addShortcutAction - addLayoutClickListener - getComponent -
getComponentNN - getComponents - getMargin -
getOwnComponent - getOwnComponents - remove - removeAll -
setMargin - setSpacing
Button button = (Button) gridLayout.getComponent(0,1);
grid.addShortcutAction(new ShortcutAction("SHIFT-A",
shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
ДЕМОНСТРАЦИЯ
setMargin - setSpacing
5.5.2.2.7. GroupBoxLayout5.5.2.2.7. GroupBoxLayout
GroupBoxLayout - контейнер,
позволяющий выделить рамкой содержащиеся в нем
компоненты, и задать им общий заголовок. Кроме того, он
умеет сворачивать свое содержимое.
XML-имя компонента: groupBox.
Пример описание контейнера в XML-дескрипторе экрана:
Атрибуты groupBox:
caption - заголовок группы.
orientation - задает направление расположения
вложенных компонентов − horizontal или vertical. По
<groupBox caption="Order">
<dateField datasource="orderDs" property="date"
caption="Date"/>
<lookupField datasource="orderDs"
property="customer"
optionsDatasource="customersDs"
caption="Customer"/>
<textField datasource="orderDs" property="amount"
caption="Amount"/>
</groupBox>
умолчанию vertical.
collapsable − значение true позволяет пользователю
скрывать содержимое компонента с помощью значков
/ .
collapsed − если указано значение true, то содержимое
компонента будет свернуто сразу после открытия
экрана. Используется совместно с collapsable="true".
Пример свернутого GroupBox:
Изменения состояния компонента groupBox
(сворачивание и разворачивание) можно отслеживать с
помощью интерфейса ExpandedStateChangeListener.
outerMargin - устанавливает внешние поля вокруг
границы groupBox. Если указано значение true, внешние
поля будут добавлены ко всем сторонам компонента.
Чтобы задать внешние поля индивидуально, укажите
значения true или false для каждой стороны groupBox:
Если атрибут showAsPanel установлен в true,
outerMargin игнорируется.
showAsPanel – если указано значение true, то
компонент будет выглядеть как Vaadin Panel. Значение
по-умолчанию - false.
Контейнер groupBox по умолчанию имеет ширину 100%
<groupBox outerMargin="true, false, true, false">
Контейнер groupBox по умолчанию имеет ширину 100%
аналогично vbox.
В веб-клиенте с темой, основанной на Halo, к компоненту
groupBox можно применить предопределенные стили.
Стили задаются в XML-дексрипторе или контроллере
экрана с помощью атрибута stylename. Чтобы применить
стиль программно, выберите одну из констант класса
HaloTheme с префиксом компонента LAYOUT_ или
GROUPBOX_. Следующие стили должны использоваться
совместно с атрибутом showAsPanel, имеющим значение
true:
стиль borderless удаляет рамку и фон контейнера
groupBox:
стиль card придаёт контейнеру вид карточки.
стиль well делает контейнер "утопленным" с
затемнением фона:
groupBox.setShowAsPanel(true);
groupBox.setStyleName(HaloTheme.GROUPBOX_PANEL_BORDE
RLESS);
<groupBox caption="Well-styled groupBox"
showAsPanel="true"
stylename="well"
width="300px"
height="200px"/>
ДЕМОНСТРАЦИЯ
В компоненте Groupbox можно использовать горячие
клавиши. Задать сочетание клавиш и вызываемое
действие можно с помощью метода addShortcutAction():
Атрибуты groupBox groupBox
align - caption - collapsable - collapsed - expand - height - id -
orientation - outerMargin - settingsEnabled - spacing - stylename -
width
Предопределенные стили groupBox groupBox
borderless - card - well
APIAPI
add - addExpandedStateChangeListener - addShortcutAction -
applySettings - getComponent - getComponentNN - getComponents
- getOwnComponent - getOwnComponents - indexOf - remove -
removeAll - saveSettings - setOuterMargin - setSpacing
5.5.2.2.8. HtmlBoxLayout5.5.2.2.8. HtmlBoxLayout
HtmlBoxLayout позволяет определять
расположение компонентов в HTML-шаблоне, который
включается в тему.
XML-имя компонента: htmlBox.
groupBox.addShortcutAction(new ShortcutAction("SHIFT-
A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
Ниже приведен пример использования htmlBox в простом
экране.
XML-дескриптор экрана:
<htmlBox align="TOP_CENTER"
template="sample"
width="500px">
<label id="logo"
value="Subscribe"
stylename="logo"/>
<textField id="email"
width="100%"
inputPrompt="email@test.test"/>
<button id="submit"
width="100%"
invoke="showMessage"
caption="Subscribe"/>
</htmlBox>
Атрибуты htmlBox:
Атрибут template задает имя HTML-файла,
находящегося в подкаталоге layouts темы. Перед
созданием шаблона необходимо создать расширение
темы или новую тему.
Например, если вы используете тему Halo и хотите
назвать шаблон my_template, укажите my_template в
атрибуте и разместите шаблон в файле
modules/web/themes/halo/layouts/my_template.html.
Содержимое шаблона
modules/web/themes/halo/layouts/sample.html:
Шаблон должен содержать элементы <div> с
атрибутами location. В этих элементах будут
отображаться компоненты CUBA, определенные в XML
дескрипторе с соответствующими идентификаторами.
Содержимое файла modules/web/themes/halo/halo-
ext.scss (в разделе Расширение существующей темы
приведена информация о том как создать этот файл):
<div location="logo" class="logo"></div>
<table class="component-container">
<tr>
<td>
<div location="email"
class="email"></div>
</td>
<td>
<div location="submit"
class="submit"></div>
</td>
</tr>
</table>
Атрибут templateContents задаёт непосредственно
содержимое шаблона, который будет использован для
отображения данного контейнера.
Пример использования атрибута:
@import "../halo/halo";
@mixin halo-ext {
@include halo;
.email {
width: 390px;
}
.submit {
width: 100px;
}
.logo {
font-size: 96px;
text-transform: uppercase;
margin-top: 50px;
}
.component-container {
display: inline-block;
vertical-align: top;
width: 100%;
}
}
<htmlBox height="256px"
width="400px">
<templateContents>
<![CDATA[
<![CDATA[
<table align="center" cellspacing="10"
style="width: 100%; height: 100%;
color: #fff; padding: 20px; background: #31629E
repeat-x">
<tr>
<td colspan="2"><h1
style="margin-top: 0;">Login</h1>
<td>
</tr>
<tr>
<td
align="right">User name:</td>
<td>
<div location="username">
</div>
</td>
</tr>
<tr>
<td align="right">Password:</td>
<td>
<div location="password">
</div>
</td>
</tr>
<tr>
<td align="right" colspan="2">
<div location="okbutton"
style="padding: 10px;"></div>
</td>
</tr>
<tr>
<td colspan="2" style="padding:
7px; background-color: #4172AE"><span
style="font-family:
FontAwesome; margin-right: 5px;"></span>
This information is in the layout.
ДЕМОНСТРАЦИЯ
Атрибуты htmlBox htmlBox
align - enable - height - id - stylename - template - templateContents
- visible - width
APIAPI
add - getComponent - getComponentNN - getComponents -
getOwnComponent - getOwnComponents - remove - removeAll
5.5.2.2.9. ScrollBoxLayout5.5.2.2.9. ScrollBoxLayout
ScrollBoxLayout − контейнер, который
позволяет прокручивать свое содержимое.
XML-имя компонента: scrollBox
Пример описание контейнера с прокруткой в XML-
дескрипторе экрана:
This information is in the layout.
<td>
</tr>
</table>
]]>
</templateContents>
<textField id="username"
width="100%"/>
<textField id="password"
width="100%"/>
<button id="okbutton"
caption="Login"/>
</htmlBox>
С помощью атрибута orientation можно задавать
направление расположения вложенных компонентов −
horizontal или vertical. По умолчанию vertical.
Атрибут scrollBars позволяет настраивать полосы
прокрутки. Может принимать значения horizontal,
vertical - для прокрутки по горизонтали и вертикали
соответственно, both - для прокрутки во всех
направлениях. Установка значения none запрещает
прокрутку в любом направлении
Вложенные в scrollBox компоненты должны иметь
фиксированные размеры или размеры по умолчанию.
Нельзя устанавливать height="100%" или
width="100%".
В то же время scrollBox не может вычислять свои
собственные размеры по содержимому. Ему нужно
либо указать абсолютные размеры, либо растянуть в
родительском контейнере, установив height="100%"
и width="100%".
В компоненте ScrollBox можно использовать горячие
<groupBox caption="Order" width="300" height="170">
<scrollBox width="100%" height="100%"
spacing="true" margin="true">
<dateField datasource="orderDs"
property="date" caption="Date"/>
<lookupField datasource="orderDs"
property="customer" optionsDatasource="customersDs"
caption="Customer"/>
<textField datasource="orderDs"
property="amount" caption="Amount"/>
</scrollBox>
</groupBox>
ДЕМОНСТРАЦИЯ
В компоненте ScrollBox можно использовать горячие
клавиши. Задать сочетание клавиш и вызываемое
действие можно с помощью метода addShortcutAction():
Атрибуты scrollBox scrollBox
align - height - id - margin - orientation - scrollBars - spacing -
stylename - width
APIAPI
add - addShortcutAction - getComponent - getComponentNN -
getComponents - getMargin - getOwnComponent -
getOwnComponents - indexOf - remove - removeAll - setMargin -
setSpacing
5.5.2.2.10. SplitPanel5.5.2.2.10. SplitPanel
SplitPanel − контейнер, разбитый на
две области, размер которых по горизонтали либо
вертикали можно менять путем перемещения
разделителя.
XML-имя компонента: split.
Пример описания панели с разделителем в XML-
дескрипторе экрана:
scrollBox.addShortcutAction(new ShortcutAction("SHIFT-
A", shortcutTriggeredEvent ->
showNotification("SHIFT-A action" )));
<split orientation="horizontal" pos="30" width="100%"
height="100%">
Внутри контейнера split обязательно должны
находиться два вложенных контейнера или компонента,
которые и будут расположены по обе стороны
разделителя.
Атрибуты split:
minSplitPosition, maxSplitPosition - определяют
диапазон допустимых значений позиции разделителя.
Могут быть установлены в пикселях или в процентах.
Например, вы можете запретить перетаскивать
сплиттер вне диапазона между 100 и 300 пикселями с
левой стороны компонента:
height="100%">
<vbox margin="true" spacing="true">
<dateField datasource="orderDs"
property="date" caption="Date"/>
<lookupField datasource="orderDs"
property="customer" optionsDatasource="customersDs"
caption="Customer"/>
</vbox>
<vbox margin="true" spacing="true">
<textField datasource="orderDs"
property="amount" caption="Amount"/>
</vbox>
</split>
<split id="splitPanel" maxSplitPosition="300px"
minSplitPosition="100px" width="100%" height="100%">
<vbox margin="true" spacing="true">
<button caption="Button 1"/>
<button caption="Button 2"/>
</vbox>
<vbox margin="true" spacing="true">
<button caption="Button 4"/>
<button caption="Button 5"/>
Если вы хотите установить диапазон программно, вы
должны указать единицу измерения с помощью
Component.UNITS_PIXELS или
Component.UNITS_PERCENTAGE
orientation - задает ориентацию расположения
компонентов. horizontal - вложенные компоненты
располагаются горизонтально, vertical - вертикально.
pos - целое число, определяющее процентное
соотношение размера первой области по отношению ко
второй. Например, pos="30" означает соотношение
областей 30/70. По умолчанию соотношение областей
составляет 50/50.
reversePosition - указывает, что атрибут pos содержит
позицию разделителя, отсчитанную с обратной стороны
компонента.
Если атрибут locked установлен в true, то пользователи
не смогут изменить положение разделителя.
Атрибут stylename со значением large увеличивает
толщину разделителя.
Методы SplitPanel:
<button caption="Button 5"/>
</vbox>
</split>
splitPanel.setMinSplitPosition(100,
Component.UNITS_PIXELS);
splitPanel.setMaxSplitPosition(300,
Component.UNITS_PIXELS);
split.setStyleName(HaloTheme.SPLITPANEL_LARGE);
ДЕМОНСТРАЦИЯ
Методы SplitPanel:
Позицию разделителя можно получить с помощью
метода getSplitPosition().
События изменения положения разделителя можно
отлеж PositionUpdateListener.
Если нужно получить единицу измерения позиции
разделителя, используйте метод
getSplitPositionUnit(). Он возвращает
Component.UNITS_PIXELS или
Component.UNITS_PERCENTAGE.
isSplitPositionReversed() возвращает true в случае,
если позиция отсчитывается с обратной стороны
компонента.
Атрибуты split split
align - height - id - locked - minSplitPosition - maxSplitPosition -
orientation - pos - reversePosition - settingsEnabled - stylename -
width
APIAPI
add - addPositionUpdateListener - applySettings - getComponent -
getComponentNN - getComponents - getOwnComponent -
getOwnComponents - remove - removeAll - saveSettings
5.5.2.2.11. TabSheet5.5.2.2.11. TabSheet
Контейнер TabSheet - это панель с
вкладками (tabs). В один момент времени отображается
содержимое только одной вкладки.
XML-имя компонента: tabSheet.
Пример описания панели с вкладками в XML-дескрипторе
экрана:
Компонент tabSheet должен иметь вложенные элементы
tab, описывающие вкладки. Каждая вкладка является
контейнером с вертикальным расположением
компонентов, аналогичным vbox.
Атрибуты элемента tab:
id - идентификатор вкладки. Следует отметить, что
вкладка не является компонентом, и данный
идентификатор используется только в рамках TabSheet
для работы с ней из кода контроллера.
caption - заголовок вкладки.
closable - определяет, будет ли отображаться кнопка xx
для закрытия вкладки. Значение по умолчанию - false.
<tabSheet>
<tab id="mainTab" caption="Tab1" margin="true"
spacing="true">
<dateField datasource="orderDs"
property="date" caption="Date"/>
<lookupField datasource="orderDs"
property="customer" optionsDatasource="customersDs"
caption="Customer"/>
</tab>
<tab id="additionalTab" caption="Tab2"
margin="true" spacing="true">
<textField datasource="orderDs"
property="amount" caption="Amount"/>
</tab>
</tabSheet>
icon - указывает на местоположение пиктограммы в
каталоге темы. Применяется только для блока WebWeb
ClientClient. Подробную информацию о том, где следует
располагать файлы пиктограмм, можно прочитать в
Создание темы приложения.
lazy - задает отложенную загрузку содержимого
вкладки.
При открытии экрана lazy-вкладки не загружают свое
содержимое, что приводит к созданию меньшего
количества компонентов в памяти. Компоненты
вкладки загружаются только в тот момент, когда
пользователь выбирает данную вкладку. Кроме того,
если на lazy-вкладке расположены визуальные
компоненты, связанные с источником данных,
содержащим JPQL запрос, то этот запрос также не
выполняется. В результате экран открывается быстрее,
а данные загружаются только в тот момент, когда
пользователь действительно хочет их увидеть, выбирая
данную вкладку.
Следует иметь в виду, что компоненты, расположенные
на lazy-вкладке, не существуют в момент открытия
экрана. Поэтому их нельзя инжектировать в
контроллер, и нельзя получить вызовом getComponent()
в методе init() контроллера. Обратиться к
компонентам lazy-вкладки можно только после того,
как пользователь на нее переключился. Этот момент
можно отловить с помощью слушателя
TabSheet.SelectedTabChangeListener, например:
@Inject
private TabSheet tabsheet;
private boolean detailsInitialized,
historyInitialized;
По умолчанию вкладки не являются lazy, а значит,
загружают свое содержимое в момент открытия экрана.
historyInitialized;
@Override
public void init(Map<String, Object> params) {
tabsheet.addSelectedTabChangeListener(event -> {
if
("detailsTab".equals(event.getSelectedTab().getName(
))) {
initDetails();
} else if
("historyTab".equals(event.getSelectedTab().getName(
))) {
initHistory();
}
});
}
private void initDetails(){
if (detailsInitialized) {
return;
}
// use getComponentNN("comp_id") here to get
tab's components
detailsInitialized=true;
}
private void initHistory(){
if (historyInitialized) {
return;
}
// use getComponentNN("comp_id") here to get
tab's components
historyInitialized = true;
}
загружают свое содержимое в момент открытия экрана.
detachable - значение true в десктоп-реализации
экрана дает возможность отсоединять вкладку в
отдельное окно. Это позволяет, например, размещать
части UI приложения на разных мониторах. Отделяемая
вкладка имеет специальную кнопку в заголовке:
Стили TabSheet TabSheet
В веб-клиенте с темой, основанной на Halo, к
контейнеру TabSheet можно применить
предопределенные стили. Стили задаются в XML-
дексрипторе или контроллере экрана с помощью
атрибута stylename:
Чтобы применить стиль программно, выберите одну из
констант класса HaloTheme с префиксом компонента
TABSHEET_:
centered-tabs - центрирует вкладки на панели.
Подходит для страниц, где все вкладки целиком
помещаются на панели (т.е. нет прокрутки вкладок).
compact-tabbar - уменьшает отступы вокруг вкладок.
equal-width-tabs - задаёт всем вкладкам на панели
<tabSheet stylename="framed">
<tab id="mainTab" caption="Framed tab"/>
</tabSheet>
tabSheet.setStyleName(HaloTheme.TABSHEET_COMPACT_TA
BBAR);
equal-width-tabs - задаёт всем вкладкам на панели
равный размер (т.е. expand ratio == 1 для всех вкладок).
Заголовки вкладок будут обрезаны, если они не
поместятся на вкладку целиком. Прокрутка вкладок в
этом случае не работает (будут видны одновременно
все вкладки).
framed - добавляет рамку как вокруг всего контейнера
целиком, так и вокруг каждой вкладки на панели.
icons-on-top - располагает пиктограмму вкладки над
её заголовком (по умолчанию пиктограммы
располагаются слева от заголовка).
only-selected-closeable - только выделенная
вкладка имеет кнопку закрытия. Стиль не запрещает
программного закрытия вкладок, а только скрывает
кнопку от пользователя.
padded-tabbar - добавляет небольшие отступы вокруг
вкладок на панели, так что они не касаются границ
контейнера.
Атрибуты tabSheet tabSheet
height - id - stylename - tabCaptionsAsHtml - tabIndex - tabsVisible -
visible - width
Атрибуты tab tab
caption - closable - detachable - enable - expand - margin - icon - id -
lazy - spacing - stylename - visible
Предопределенные стили tabSheet tabSheet
centered-tabs - compact-tabbar - equal-width-tabs - framed - framed
- only-selected-closeable - padded-tabbar
APIAPI
add - addSelectedTabChangeListener - getComponent -
getComponentNN - getComponents - getOwnComponent -
getOwnComponents - remove - removeAll
5.5.2.3. 5.5.2.3. Разное
В данном разделе рассматриваются различные элементы
универсального пользовательского интерфейса,
имеющие отношение к визуальным компонентам.
5.5.2.3.1. Formatter5.5.2.3.1. Formatter
Formatter предназначен для преобразования некоторого
значения в строку.
Formatter предназначен для использования с read-only
компонентами, такими как Label, колонка Table и тому
подобными. Для форматирования значения в
редактируемых компонентах, например TextField,
используйте механизм Datatype.
В XML-дескрипторе экрана formatter для компонента может
быть задан во вложенном элементе formatter. Элемент
имеет единственный атрибут:
class − имя класса, реализующего интерфейс
com.haulmont.cuba.gui.components.Formatter
Если конструктор класса formatter принимает параметр
типа org.dom4j.Element, то ему будет передан элемент
XML, описывающий данный formatter. Это можно
использовать для параметризации экземпляра formatter'а,
например, строкой форматирования. В частности,
имеющиеся в платформе классы DateFormatter и
NumberFormatter могут брать строку форматирования из
атрибута format. Пример использования:
<column id="date">
<formatter
class="com.haulmont.cuba.gui.components.formatters.Dat
Кроме того, класс DateFormatter распознает также
атрибут type, который может принимать значения DATE
или DATETIME. В этом случае форматирование
производится с помощью механизма Datatype по строке
формата dateFormat или dateTimeFormat соответственно.
Например:
Если formatter реализован внутренним классом, то он должен быть
объявлен с модификатором static, а его имя для загрузки
отделяется символом "$", например:
<formatter
class="com.sample.sales.gui.OrderBrowse$CurrencyFormatter"/>
Formatter можно назначить компоненту не только в XML-
дескрипторе экрана, но и программно, передавая
экземпляр formatter'а в метод setFormatter() компонента.
Пример объявления собственного formatter'а и
использования его для форматирования значения
колонки таблицы:
class="com.haulmont.cuba.gui.components.formatters.Dat
eFormatter" format="yyyy-MM-dd HH:mm:ss"/>
</column>
<column id="endDate">
<formatter
class="com.haulmont.cuba.gui.components.formatters.Dat
eFormatter" type="DATE"/>
</column>
public class CurrencyFormatter implements
Formatter<BigDecimal> {
5.5.2.3.2. Presentations5.5.2.3.2. Presentations
Механизм представлений позволяет пользователям
системы управлять настройками отображения таблиц.
protected GeneralConfiguration
generalConfiguration;
protected Currency currentCurrency;
public CurrencyFormatter(GeneralConfiguration
generalConfiguration) {
this.generalConfiguration =
generalConfiguration;
currentCurrency =
generalConfiguration.getCurrency();
}
@Override
public String format(BigDecimal value) {
return currentCurrency.format(value);
}
}
protected void initTableColumns() {
Formatter<BigDecimal> currencyFormatter = new
CurrencyFormatter(generalConfiguration);
table.getColumn("totalPrice").setFormatter(currencyFor
matter);
}
Пользователи могут:
Сохранять представления под уникальными именами.
Настройки таблицы автоматически сохраняются в
активном представлении.
Редактировать и удалять представления.
Переключаться между представлениями.
Задавать представление по умолчанию, которое будет
применяться при открытии экрана.
Создавать глобальные представления, доступные всем
пользователям системы. Для создания, изменения и
удаления глобальных представлений пользователь
должен иметь разрешение
cuba.gui.presentations.global.
Представления доступны в компонентах, реализующих
интерфейс
com.haulmont.cuba.gui.components.Component.HasPrese
ntations. В платформе такими компонентами являются:
Table
GroupTable
TreeTable
5.5.2.3.3. Timer5.5.2.3.3. Timer
Таймер − это невизуальный компонент, позволяющий
выполнять некоторый код контроллера экрана через
определенные промежутки времени. Срабатывание
таймера происходит в потоке обработки событий
пользовательского интерфейса, что позволяет обновлять
экран без каких-либо ограничений. Таймер прекращает
работу при закрытии экрана, для которого он был создан.
работу при закрытии экрана, для которого он был создан.
Компонент реализован для блоков Web ClientWeb Client и DesktopDesktop
ClientClient. Для веб клиента реализация таймеров основана на
опросе сервера из веб-браузера, для десктоп клиента - на
javax.swing.Timer.
Основной способ создания таймеров - декларативно в
XML-дескрипторе экрана в элементе timers,
располагающемся между элементами dsContext и layout.
Для описания таймера используется элемент timer.
Атрибут delay является обязательным атрибутом, в нем
задается интервал срабатывания таймера в
миллисекундах.
autostart - необязательный атрибут, при установке
которого в true таймер стартует сразу после открытия
экрана. По умолчанию false, что означает что для
старта таймера необходимо вызвать его метод start().
repeating − необязательный атрибут, включает
многократное срабатывание таймера. Если значение
атрибута равно true, то таймер выполняется
циклически, через равные промежутки времени,
заданные в атрибуте delay. В противном случае таймер
выполняется один раз через delay миллисекунд после
старта таймера.
onTimer − необязательный атрибут, содержащий имя
метода, вызываемого при срабатывании таймера.
Метод-обработчик должен быть определен в
контроллере экрана с модификатором public и иметь
один параметр типа
com.haulmont.cuba.gui.components.Timer.
Пример использования таймера для периодического
обновления содержимого таблицы:
Таймер можно инжектировать в поле контроллера, либо
получить методом Window.getTimer(). Управлять
активностью таймера можно с помощью его методов
start() и stop(). Для уже активного таймера вызов
start() игнорируется. После остановки таймера методом
stop() его можно снова запустить методом start().
Пример определения таймера в XML дескрипторе и
использования листенеров в контроллере:
<window ...
<dsContext>
<collectionDatasource id="bookInstanceDs" ...
</dsContext>
<timers>
<timer delay="3000" autostart="true"
repeating="true" onTimer="refreshData"/>
</timers>
<layout ...
@Inject
private CollectionDatasource bookInstanceDs;
public void refreshData(Timer timer) {
bookInstanceDs.refresh();
}
<timers>
<timer id="helloTimer" delay="5000"/>
</timers>
@Inject
private Timer helloTimer;
Таймер можно также создавать в коде контроллера
следующим образом:
@Override
public void init(Map<String, Object> params) {
// add execution handler
helloTimer.addActionListener(timer -> {
showNotification("Hello",
NotificationType.HUMANIZED);
});
// add stop listener
helloTimer.addStopListener(timer -> {
showNotification("Timer is stopped",
NotificationType.HUMANIZED);
});
// start the timer
helloTimer.start();
}
@Inject
private ComponentsFactory componentsFactory;
@Override
public void init(Map<String, Object> params) {
// create timer
Timer helloTimer =
componentsFactory.createTimer();
// add timer to the screen
addTimer(helloTimer);
// set timer parameters
helloTimer.setDelay(5000);
helloTimer.setRepeating(true);
// add execution handler
helloTimer.addActionListener(timer -> {
showNotification("Hello",
NotificationType.HUMANIZED);
5.5.2.3.4. Validator5.5.2.3.4. Validator
Валидатор предназначен для проверки значения,
введенного в визуальном компоненте.
Следует отличать валидацию от проверки типа
данных. Если для некоторого компонента, например
TextField, задан тип, отличный от строкового (это
происходит при связывании с атрибутом сущности
или назначении datatype), то компонент не
позволяет ввести значение, не удовлетворяющее
этому типу - при потере фокуса или нажатии EnterEnter
компонент отобразит предыдущее значение.
Валидация же срабатывает не сразу при вводе или
потере компонентом фокуса, а только при вызове у
компонента метода validate(). Это означает, что
компонент (и связанный с ним атрибут сущности)
может некоторое время содержать значение, не
удовлетворяющее условиям валидации. Это не
является проблемой, так как обычно компоненты
ввода с валидацией располагаются в экране
редактирования, а он автоматически вызывает
валидацию всех своих компонентов перед коммитом.
Если же компонент находится не в экране
NotificationType.HUMANIZED);
});
// add stop listener
helloTimer.addStopListener(timer -> {
showNotification("Timer is stopped",
NotificationType.HUMANIZED);
});
// start the timer
helloTimer.start();
}
Если же компонент находится не в экране
редактирования, то необходимо вызывать его метод
validate() в контроллере явно.
В XML-дескрипторе экрана валидатор для компонента
может быть задан во вложенном элементе validator.
Возможные атрибуты элемента validator:
script − путь к скрипту Groovy, осуществляющему
валидацию.
class − имя класса Java, реализующего интерфейс
Field.Validator.
Groovy-валидатор и стандартные классы Java-валидаторов,
расположенные в пакете
com.haulmont.cuba.gui.components.validators
поддерживают атрибут message − сообщение, выводимое
пользователю в случае ошибки валидации. Атрибут
должен содержать сообщение или ключ в пакете
сообщений экрана, например:
Валидатор можно установить с помощью интерфейса
CUBA Studio. Пример добавления валидатора к полю
fieldGroup:
<validator
class="com.haulmont.cuba.gui.components.validators.Pat
ternValidator"
message="msg://validationError"
pattern="\d{3}"/>
# messages.properties
validationError = Input error
Выбор механизма валидации осуществляется следующим
образом:
Если не указано значение атрибута script, и сам
элемент validator не содержит текста выражения
Groovy, то в качестве валидатора используется класс,
указанный в атрибуте class.
Если элемент validator содержит текст, то он будет
использован как выражение Groovy и выполнен с
помощью Scripting.
<field property="amount">
<validator
class="com.haulmont.cuba.gui.components.validators.D
oubleValidator"/>
</field>
<field property="year">
<validator>
value ==~ /\d+/
</validator>
В противном случае с помощью Scripting будет выполнен
скрипт Groovy, указанный в атрибуте script.
В выражение или скрипт Groovy будет передана одна
переменная value, содержащая значение, введенное в
визуальном компоненте. Выражение или скрипт должны
вернуть boolean значение: true − valid, false − not valid.
Если в качестве валидатора используется класс Java, то он
должен иметь либо дефолтный конструктор без
параметров, либо конструктор со следующим набором
параметров:
org.dom4j.Element, String - в этот конструктор будут
переданы XML-элемент валидатора и имя пакета
сообщений экрана.
org.dom4j.Element - в этот конструктор будет передан
XML-элемент валидатора.
Если валидатор реализован внутренним классом, то он
должен быть объявлен с модификатором static, а его имя
для загрузки отделяется символом "$", например:
Платформа уже содержит несколько реализаций
</field>
<field property="zipCode">
<validator
script="com.company.demo.web.address.ZipValidator"/>
</field>
<validator
class="com.sample.sales.gui.AddressEdit$ZipValidator"/>
Платформа уже содержит несколько реализаций
наиболее часто используемых валидаторов (см. пакет
com.haulmont.cuba.gui.components.validators), которые
можно применять в проектах:
DateValidator
DoubleValidator
EmailValidator
IntegerValidator
LongValidator
PatternValidator
ScriptValidator
StringValidator
Валидатор-класс можно назначить компоненту не только
в XML-дескрипторе экрана, но и программно, передавая
экземпляр валидатора в метод addValidator()
компонента.
Пример создания класса валидатора почтового индекса:
Использование валидатора почтового индекса и
стандартного валидатора по шаблону в полях компонента
FieldGroup:
public class ZipValidator implements Field.Validator {
@Override
public void validate(Object value) throws
ValidationException {
if (value != null && ((String) value).length()
!= 6)
throw new ValidationException("Zip must be
of 6 characters length");
}
}
Пример программного задания валидатора:
5.5.2.4. API 5.5.2.4. API компонентов
<fieldGroup>
<field property="zip" required="true">
<validator
class="com.company.sample.gui.ZipValidator"/>
</field>
<field property="imei">
<validator
class="com.haulmont.cuba.gui.components.validators.Pat
ternValidator"
pattern="\d{15}"
message="IMEI validation failed"/>
</field>
</fieldGroup>
if (Boolean.TRUE.equals(parameter.getRequired())) {
tokenList.addValidator(new Field.Validator() {
@Override
public void validate(Object value) throws
ValidationException {
if (value instanceof Collection &&
CollectionUtils.isEmpty((Collection) value)) {
throw new
ValidationException(getMessage("paramIsRequiredButEmpt
y"));
}
}
});
}
Доступно для всех визуальных компонентов
unwrap() - возвращает экземпляр компонента для
текущего типа клиента (компонент Vaadin или Swing).
Можно использовать в клиентском модуле для
доступа к API базового компонента, см. раздел Работа с
компонентами Vaadin.
unwrapComposition() - возвращает экземпляр самого
внешнего контейнера для текущего типа клиента.
Можно использовать в клиентском модуле для
доступа к API базового компонента.
Component.BufferedComponent.Buffered
commit() - обновляет источник данных, сохраняя все
изменения, внесённые после последнего коммита.
discard() - отменяет все изменения, внесённые после
последнего коммита. Значение компонента
обновляется из источника данных.
isModified() - возвращает true, если значение
компонента изменилось с момента последнего
обновления из источника данных.
Доступно для компонентов:
CheckBox - DateField - FileUploadField - LookupField -
LookupPickerField - PickerField - TextArea - TextField - TimeField
Component.CollapsableComponent.Collapsable
com.vaadin.ui.TextField vTextField =
textField.unwrap(com.vaadin.ui.TextField.class);
if (textArea.isModified()) {
textArea.commit();
}
Component.CollapsableComponent.Collapsable
addExpandedStateChangeListener() - добавляет
слушатель, реализующий интерфейс
ExpandedStateChangeListener, для отслеживания
событий сворачивания/разворачивания компонента.
Доступно для компонентов:
Filter - GroupBoxLayout
Component.ContainerComponent.Container
add() - добавляет дочерний компонент в контейнер.
remove() - удаляет дочерний компонент из
контейнера.
removeAll() - удаляет все дочерние компоненты из
контейнера.
getOwnComponent() - возвращает компонент,
вложенный непосредственно в этот контейнер.
getComponent() - возвращает компонент, находящийся
где-либо внутри дерева компонентов в этом
контейнере.
getComponentNN() - возвращает компонент,
находящийся где-либо внутри дерева компонентов в
этом контейнере, и выбрасывает исключение, если
компонент не найден.
getOwnComponents() - возвращает список всех
компонентов, вложенных непосредственно в этот
контейнер.
getComponents() - возвращает список всех
компонентов, находящихся где-либо внутри дерева
groupBox.addExpandedStateChangeListener(e ->
showNotification("Expanded: " +
groupBox.isExpanded()));
компонентов в этом контейнере.
Доступно для компонентов:
Accordion - BoxLayout - CssLayout - Frame - GridLayout -
GroupBoxLayout - HtmlBoxLayout - ScrollBoxLayout - SplitPanel -
TabSheet
Component.OrderedContainerComponent.OrderedContainer
indexOf() - вовзращает индекс компонента внутри
упорядоченного контейнера.
Доступно для компонентов:
BoxLayout - CssLayout - Frame - GroupBoxLayout - ScrollBoxLayout
-
Component.HasSettingsComponent.HasSettings
applySettings() - восстанавливает последние
пользовательские настройки для этого компонента.
saveSettings() - сохраняет текущие
пользовательские настройки для этого компонента.
Доступно для компонентов:
DataGrid - Filter - GroupBoxLayout - SplitPanel - Table - TextArea
Component.HasValueComponent.HasValue
addValueChangeListener() - добавляет слушатель,
реализующий интерфейс ValueChangeListener, для
отслеживания изменения значения компонента.
Доступно для компонентов:
CheckBox - ColorPicker - DateField - DatePicker - FileUploadField -
Label - LookupField - LookupPickerField - MaskedField -
OptionsGroup - OptionsList - PasswordField - PickerField -
textField.addValueChangeListener(e ->
showNotification("Before: " +
e.getPrevValue() + ". After: " + e.getValue()));
OptionsGroup - OptionsList - PasswordField - PickerField -
ProgressBar - RichTextArea - SearchPickerField - SourceCodeEditor
- SuggestionPickerField - TextArea - TextField - TimeField -
TokenList - TwinColumn
Component.LayoutClickNotifierComponent.LayoutClickNotifier
addLayoutClickListener() - добавляет слушатель,
реализующий интерфейс LayoutClickListener, для
отслеживания кликов по области компонента.
Доступно для компонентов:
ButtonsPanel - BoxLayout - CssLayout - GridLayout
Component.MarginComponent.Margin
setMargin() - устанавливает компоненту внешние
поля.
Добавление внешних полей со всех сторон
компонента:
Добавление внешних полей только в верхней и
нижней части компонента:
Создание объекта конфигурации MarginInfo:
vbox.addLayoutClickListener(event ->
showNotification("Clicked"));
vbox.setMargin(true);
vbox.setMargin(true, false, true, false);
vbox.setMargin(new MarginInfo(true, false, false,
true));
getMargin() - возвращает конфигурацию внешних
полей в виде экземпляра MarginInfo.
Доступно для компонентов:
BoxLayout - Filter - Frame - GridLayout - ScrollBoxLayout
Component.OuterMarginComponent.OuterMargin
setOuterMargin() - устанавливает внешние поля
вокруг границы компонента.
Добавление внешних полей со всех сторон
компонента:
Добавление внешних полей только в верхней и
нижней части компонента:
Создание объекта конфигурации MarginInfo:
getOuterMargin() - возвращает конфигурацию
внешних полей в виде экземпляра MarginInfo.
Доступно для компонентов:
GroupBoxLayout
Component.ShortcutNotifierComponent.ShortcutNotifier
addShortcutAction() - добавляет действие,
вызываемое при нажатии определённого сочетания
клавиш.
groupBox.setOuterMargin(true);
groupBox.setOuterMargin(true, false, true, false);
groupBox.setOuterMargin(new MarginInfo(true,
false, false, true));
Доступно для компонентов:
BoxLayout - ButtonsPanel - CssLayout - GridLayout -
GroupBoxLayout - ScrollBoxLayout
Component.SpacingComponent.Spacing
setSpacing() - добавляет внутренние поля между
компонентом и вложенными в него компонентами.
Доступно для компонентов:
BoxLayout - ButtonsPanel - Frame - GridLayout GroupBoxLayout -
ScrollBoxLayout
5.5.2.5. XML-5.5.2.5. XML-атрибуты компонентов
alignalign
Атрибут, задающий расположение компонента
относительно вышестоящего контейнера.
Возможные значения:
TOP_RIGHT
TOP_LEFT
TOP_CENTER
MIDDLE_RIGHT
MIDDLE_LEFT
MIDDLE_CENTER
cssLayout.addShortcutAction(new
ShortcutAction("SHIFT-A", shortcutTriggeredEvent -
>
showNotification("SHIFT-A action" )));
vbox.setSpacing(true);
MIDDLE_CENTER
BOTTOM_RIGHT
BOTTOM_LEFT
BOTTOM_CENTER
captioncaption
Атрибут, устанавливающий заголовок для визуального
компонента.
Значением атрибута должна быть либо строка
сообщения, либо ключ в пакете сообщений. В случае
ключа значение должно начинаться с префикса msg://
Способы задания ключа:
Короткий ключ − при этом сообщение ищется в пакете,
заданном для данного экрана:
Полный ключ, с заданием пакета:
captionPropertycaptionProperty
Задает имя атрибута сущности, отображаемого
компонентом. Используется только для сущностей,
находящихся в источнике данных (например заданном
для LookupField свойством optionsDatasource).
Если captionProperty не задан, будет отображаться имя
экземпляра.
colspancolspan
Указывает, сколько колонок сетки должен занять
компонент (по умолчанию 1).
caption="msg://infoFieldCaption"
caption="msg://com.company.sample.gui.screen/infoFi
eldCaption"
Данный атрибут может быть назначен любому
компоненту, находящемуся непосредственно внутри
контейнера GridLayout.
datasourcedatasource
Предназначен для задания источника данных,
описанного в секции dsContext XML-дескриптора
экрана.
При указании атрибута datasource для компонента,
реализующего интерфейс DatasourceComponent,
необходимо также задать атрибут property.
descriptiondescription
Атрибут, задающий текст подсказки для компонента.
editableeditable
Атрибут, указывающий на возможность редактирования
содержимого компонента (не путать с enable).
Возможные значения − true, false. По умолчанию true.
На возможность редактирования содержимого для
компонента, связанного с данными (наследника
DatasourceComponent или ListComponent), влияет также
подсистема безопасности. Если по данным подсистемы
безопасности данный компонент должен быть
недоступен для редактирования, значение атрибута
editable не принимается во внимание.
enableenable
Атрибут компонента, устанавливающий его состояние:
доступен, недоступен.
Если компонент недоступен, то он не принимает фокус
ввода. Недоступность контейнера приводит к тому, что
все его компоненты также становятся недоступными.
Возможные значения − true, false. По умолчанию все
компоненты доступны.
expandexpand
Атрибут контейнера для управления его внутренней
компоновкой.
Задает компонент внутри контейнера, который
необходимо расширить на все доступное пространство в
направлении размещения компонентов. Для контейнера
с вертикальным размещением устанавливает
компоненту 100% высоту, для контейнера с
горизонтальным размещением - 100% ширину. Кроме
того, при изменении размера контейнера изменять
размер будет именно этот компонент.
heightheight
Атрибут, устанавливающий высоту компонента.
Может быть задана в пикселях либо в процентах от
высоты вышестоящего контейнера. Например: 100px,
100%, 50. Если единица измерения не указана,
подразумевается высота в пикселях.
Установка значения в % означает, что компонент по
высоте займет соответствующую часть пространства,
предоставляемого контейнером более высокого уровня.
При выборе значения AUTO или -1px для компонента
устанавливается высота по умолчанию, для контейнера
высота определяется по содержимому, то есть
суммарной высотой вложенных компонентов.
iconicon
Атрибут, устанавливающий пиктограмму для
визуального компонента.
Значением атрибута должен быть путь к файлу
пиктограммы относительно каталога темы. Например:
icon="icons/create.png"
Если пиктограмма должна быть выбрана в зависимости
от языка пользователя, можно указать путь к ней в
пакете сообщений, а в атрибуте icon − ключ сообщения,
например:
В веб клиенте с темой Halo (или производной от нее)
вместо файлов можно использовать элементы шрифта
Font Awesome. Для этого достаточно указать константу из
класса com.vaadin.server.FontAwesome с префиксом
font-icon: например:
idid
Идентификатор компонента.
Рекомендуется формировать значение по правилам Java-
идентификаторов и использовать camelСase, например,
userGrid, filterPanel.Может быть указан для любого
компонента и должен быть уникальным в пределах
экрана.
inputPromptinputPrompt
Атрибут inputPrompt задает строку, отображаемую в
поле, если его значение равно null.
Атрибут используется для компонентов TextField,
LookupField, LookupPickerField, SearchPickerField,
SuggestionPickerField только в веб клиенте.
icon="msg://addIcon"
icon="font-icon:BOOK"
<suggestionField inputPrompt="Let's search
something!"/>
SuggestionPickerField только в веб клиенте.
marginmargin
Атрибут margin устанавливает наличие отступа между
внешними границами и содержимым контейнера.
Может иметь два вида значений:
margin="true" − установить отступ со всех сторон
сразу
margin="true,false,true,false" − установить отступ
только сверху и снизу (формат значения
"сверху,справа,снизу,слева")
По умолчанию отступы отсутствуют.
nullNamenullName
Идентификатор опции, выбор которой будет равносилен
установке значения в null.
Атрибут используется для компонентов LookupField,
LookupPickerField, SearchPickerField.
Пример для компонента LookupField, установка значения
атрибута в XML-дескрипторе:
Пример для компонента LookupField, установка значения
атрибута в контроллере:
<lookupField datasource="orderDs"
property="customer"
nullName="(none)"
optionsDatasource="customersDs"
width="200px"/>
<lookupField id="customerLookupField"
optionsDatasource="customersDs"
width="200px" datasource="orderDs"
property="customer"/>
openTypeopenType
Задает режим открытия связанного экрана.
Соответствует перечислению WindowManager.OpenType
со значениями NEW_TAB, THIS_TAB, NEW_WINDOW, DIALOG. По
умолчанию THIS_TAB.
optionsDatasourceoptionsDatasource
Задает имя источника данных, используемого для
формирования списка опций.
Совместно с optionsDatasource может использоваться
атрибут captionProperty.
propertyproperty
Атрибут компонента, реализующего интерфейс
DatasourceComponent.
Предназначен для задания имени атрибута сущности,
значение которого будет отображаться или
редактироваться данным визуальным компонентом.
Используется всегда совместно с атрибутом datasource.
requiredrequired
Атрибут визуального компонента, реализующего
интерфейс Field. Указывает, что в данное поле
обязательно должно быть введено значение.
Возможные значения атрибута − true, false. По
умолчанию false.
Совместно с required может использоваться атрибут
requiredMessage.
requiredMessagerequiredMessage
XML-атрибут, используемый совместно с атрибутом
property="customer"/>
customerLookupField.setNullOption("<null>");
XML-атрибут, используемый совместно с атрибутом
required. Позволяет установить сообщение, выводимое
пользователю в случае нарушения требования required.
Атрибут может содержать сообщение или ключ в пакете
сообщений, например:
requiredMessage="msg://infoTextField.requiredMessa
ge"
responsiveresponsive
Определяет, должен ли компонент реагировать на
изменения размеров доступной области. Реакцию можно
задать с помощью стилей.
Возможные значения атрибута − true, false. По
умолчанию false.
rowspanrowspan
Указывает, сколько строк сетки должен занять
компонент (по умолчанию 1).
Данный атрибут может быть назначен любому
компоненту, находящемуся непосредственно внутри
контейнера GridLayout.
settingsEnabledsettingsEnabled
Определяет, нужно ли сохранять пользовательские
настройки отображения компонента. Настройки
сохраняются только для компонентов, имеющих id.
Возможные значения атрибута − true, false. По
умолчанию true.
spacingspacing
Атрибут spacing устанавливает наличие отступов
между компонентами внутри контейнера.
Возможные значения − true, false.
По умолчанию отступы отсутствуют.
stylenamestylename
Атрибут, задающий имя стиля компонента. Подробнее
Атрибут, задающий имя стиля компонента. Подробнее
см. Темы приложения.
В теме halo определено несколько стандартных стилей
для компонентов:
huge - устанавливает размер поля 160% от его размера
по умолчанию.
large - устанавливает размер поля 120% от его размера
по умолчанию.
small - устанавливает размер поля 85% от его размера
по умолчанию.
tiny - устанавливает размер поля 75% от его размера
по умолчанию.
tabCaptionsAsHtmltabCaptionsAsHtml
Определяет, разрешена ли HTML-разметка в заголовках
вкладок. Если выбрано значение true, заголовки
отображаются в браузере как HTML, при этом
ответственность за безопасность используемого HTML-
кода несёт сам разработчик. Если выбрано значение
false, содержимое заголовка отображается как
обычный текст.
Возможные значения − true, false. По умолчанию false.
tabIndextabIndex
Определяет, может ли компонент принимать фокус, и
задаёт относительный порядок перехода фокуса между
компонентами экрана.
Может принимать положительное или отрицательное
целочисленное значение:
отрицательное значение означает, что компонент
может принимать фокус, но будет пропущен при
последовательном перемещении фокуса с
клавиатуры;
0 означает, что компонент может принимать фокус, в
0 означает, что компонент может принимать фокус, в
том числе и при его перемещении с клавиатуры, но его
относительный порядковый номер будет совпадать с
его расположением на экране относительно других
компонентов.
положительное значение означает, что компонент
может принимать фокус, в том числе и при его
перемещении с клавиатуры. Относительный
порядковый номер компонента будет совпадать со
значением атрибута: фокус будет перемещаться от
меньшего значения tabIndex к большему. Если для
нескольких компонентов установлено одинаковое
значение tabIndex, порядок их фокуса будет будет
совпадать с их расположением на экране
относительно друг друга.
tabsVisibletabsVisible
Определяет, должна ли область выбора вкладок
отображаться в UI.
Возможные значения − true, false. По умолчанию true.
textSelectionEnabledtextSelectionEnabled
Определяет, разрешено ли выделение текста в ячейках
таблицы.
Возможные значения атрибута − true, false. По
умолчанию false.
visiblevisible
Атрибут, устанавливающий видимость компонента.
Возможные значения − true, false.
Если контейнер невидим, не видны и все его
компоненты. По умолчанию все компоненты видимы.
widthwidth
Атрибут, устанавливающий ширину компонента.
Значение может быть задано в пикселях или в
процентах от ширины вышестоящего контейнера.
процентах от ширины вышестоящего контейнера.
Например: 100px, 100%, 50. Если единица измерения не
указана, подразумевается ширина в пикселях.
Простановка значения в % означает, что компонент по
ширине займет соответствующую часть пространства,
предоставляемого контейнером более высокого уровня.
При выборе значения AUTO или -1px для компонента
устанавливается ширина по умолчанию, для контейнера
ширина определяется по содержимому, то есть
суммарной шириной вложенных компонентов.
5.5.3. 5.5.3. Источники данных
Источники данных обеспечивают работу связанных с
данными (data-aware) компонентов.
Визуальные компоненты сами не обращаются к MiddlewareMiddleware,
а получают экземпляры сущностей из связанных
источников данных. При этом один источник данных
может обслуживать несколько визуальных компонентов,
если им нужен один и тот же экземпляр или набор
экземпляров.
Связь визуального компонента и источника данных
проявляется в следующем:
При изменении пользователем значения в компоненте
новое значение проставляется в атрибуте сущности,
находящейся в источнике.
При изменении атрибута сущности из кода новое
значение проставляется и отображается в визуальном
компоненте.
Для слежения за вводом пользователя можно
использовать как слушатель источника данных, так и
слушатель значения визуального компонента - они
срабатывают друг за другом.
При необходимости прочитать или записать значение
При необходимости прочитать или записать значение
атрибута сущности в коде предпочтительнее
использовать источник данных, а не компонент.
Рассмотрим пример чтения атрибута:
Как видно из примера, работа со значениями атрибутов
сущностей через компонент - не самый прямолинейный
способ. В первом случае он требует приведения типа и
указания поля FieldGroup в виде строки. Второй пример
более безопасный и прямой, но он требует знания ещё и
типа поля, которое должно быть инжектировано в
контроллер. В то же время, получив методом getItem()
из источника данных хранящийся в нем экземпляр,
@Inject
private FieldGroup fieldGroup;
@Inject
private Datasource<Order> orderDs;
@Named("fieldGroup.customer")
private PickerField customerField;
public void init(Map<String, Object> params){
Customer customer;
// Get customer from component: not for common
use
Component component =
fieldGroup.getFieldNN("customer").getComponentNN();
customer = ((HasValue)component).getValue();
// Get customer from component: recommended
customer = customerField.getValue();
// Get customer from datasource
customer = orderDs.getItem().getCustomer();
}
можно напрямую читать и изменять значения его
атрибутов.
Как правило, визуальный компонент привязывается к
атрибуту, непосредственно принадлежащему
сущности, находящейся в источнике данных. В
приведенном выше примере компонент привязан к
атрибуту customer сущности Order.
Можно также привязать компонент к атрибуту
связанной сущности, например к customer.name. В
этом случае компонент будет корректно отображать
значение атрибута name, но при его изменении
пользователем слушатели источника данных
вызваны не будут, и изменения не будут сохранены.
Поэтому привязывать компонент к атрибутам
второго и более порядка имеет смысл только для
отображения, например в Label, колонке Table или
установив для TextField свойство editable = false.
Источники данных также отслеживают изменения
содержащихся в них сущностей, и могут отправлять
измененные экземпляры обратно на MiddlewareMiddleware для
сохранения в базе данных.
Рассмотрим основные интерфейсы источников.
Рисунок 22. 22. Интерфейсы источников данных
Datasource − простейший источник данных,
Datasource − простейший источник данных,
предназначенный для работы с одним экземпляром
сущности. Экземпляр устанавливается методом
setItem() и доступен через getItem().
Стандартной реализацией такого источника является
класс DatasourceImpl, который используется, например,
как главный источник данных в экранах
редактирования сущностей.
CollectionDatasource − источник данных,
предназначенный для работы с коллекцией
экземпляров сущности. Коллекция загружается при
вызове метода refresh(), ключи экземпляров доступны
через метод getItemIds(). Метод setItem()
устанавливает, а getItem() возвращает "текущий"
экземпляр коллекции, т.е., например, соответствующий
выбранной в данный момент строке таблицы.
Способ загрузки коллекции сущностей определяется
реализацией. Наиболее типичный - загрузка с Middleware
через DataManager, при этом для формирования JPQL
запроса используются методы setQuery(),
setQueryFilter().
Стандартной реализацией такого источника является
класс CollectionDatasourceImpl, который используется
в экранах, отображающих списки сущностей.
GroupDatasource − подвид CollectionDatasource,
предназначенный для работы с компонентом
GroupTable.
Стандартной реализацией является класс
GroupDatasourceImpl.
HierarchicalDatasource − подвид
CollectionDatasource, предназначенный для работы с
компонентами Tree и TreeTable.
Стандартной реализацией является класс
HierarchicalDatasourceImpl.
NestedDatasource - источник данных, предназначенный
для работы с экземплярами, загруженными в атрибуте
другой сущности. При этом источник, содержащий
сущность-хозяина, доступен методом getMaster(), а
мета-свойство, соответствующее атрибуту хозяина,
содержащему экземпляры данного источника, доступно
через метод getProperty().
Например, в источнике dsOrder установлен экземпляр
сущности Order, содержащий ссылку на экземпляр
Customer. Тогда для связи экземпляра Customer с
визуальными компонентами достаточно создать
NestedDatasource, у которого хозяином является
dsOrder, а мета-свойство указывает на атрибут
Order.customer.
PropertyDatasource - подвид NestedDatasource,
предназначенный для работы с одним экземпляром или
коллекцией связанных сущностей, не являющихся
встроенными (embedded).
Стандартные реализации: для работы с одним
экземпляром - PropertyDatasourceImpl, для работы с
коллекцией - CollectionPropertyDatasourceImpl,
GroupPropertyDatasourceImpl,
HierarchicalPropertyDatasourceImpl. Последние
реализуют также интерфейс CollectionDatasource,
однако некоторые его нерелевантные методы,
связанные с загрузкой, например, setQuery(),
выбрасывают UnsupportedOperationException.
EmbeddedDatasource - подвид NestedDatasource,
содержащий экземпляр встроенной сущности.
Стандартной реализацией является класс
EmbeddedDatasourceImpl.
RuntimePropsDatasource − специфический источник,
RuntimePropsDatasource − специфический источник,
предназначенный для работы с динамическими
атрибутами сущностей.
Как правило, источники данных объявляются
декларативно в секции dsContext дескриптора экрана.
5.5.3.1. 5.5.3.1. Создание источников данных
Объекты источников данных могут быть созданы как
декларативно - путем объявления в XML-дескрипторе
экрана, так и программно в контроллере. Обычно
используются стандартные реализации интерфейсов
источников, однако при необходимости можно создать
собственный класс, унаследовав его от стандартного.
5.5.3.1.1. 5.5.3.1.1. Декларативное создание
Как правило, источники данных объявляются
декларативно в элементе dsContext дескриптора экрана.
В зависимости от взаимного расположения элементов
объявлений создаются источники двух разновидностей:
если элемент расположен непосредственно в
dsContext, создается обычный Datasource или
CollectionDatasource, который содержит независимо
загруженную сущность или коллекцию;
если элемент расположен внутри элемента другого
источника, создается NestedDatasource, при этом
внешний источник становится его хозяином.
Пример объявления источников данных:
<dsContext>
<datasource id="carDs"
class="com.haulmont.sample.entity.Car" view="carEdit">
<collectionDatasource id="allocationsDs"
property="driverAllocations"/>
Здесь источник carDs содержит один экземпляр сущности
Car, а вложенные в него allocationsDs и repairsDs
содержат коллекции связанных сущностей из атрибутов
Car.driverAllocations и Car.repairs соответственно.
Экземпляр Car вместе со связанными сущностями
проставляется в источник данных извне. Если данный
экран является экраном редактирования, то это
происходит автоматически при открытии экрана.
Источник данных colorsDs содержит коллекцию
экземпляров сущности Color, загружаемую самим
источником по указанному JPQL-запросу с
представлением _local.
Рассмотрим схему XML.
dsContext - корневой элемент.
Элементы dsContext:
datasource - определяет источник данных, содержащий
единственный экземпляр сущности.
Атрибуты:
id - идентификатор источника, должен быть
property="driverAllocations"/>
<collectionDatasource id="repairsDs"
property="repairs"/>
</datasource>
<collectionDatasource id="colorsDs"
class="com.haulmont.sample.entity.Color"
view="_local">
<query>
<![CDATA[select c from sample$Color c
order by c.name]]>
</query>
</collectionDatasource>
</dsContext>
id - идентификатор источника, должен быть
уникальным для данного DsContext.
class - Java класс сущности, которая будет содержаться
в данном источнике
view - имя представления сущности. Если источник сам
загружает экземпляры, то это представление будет
использовано при загрузке. В противном случае это
представление сигнализирует внешним механизмам о
том, как нужно загрузить сущность для данного
источника.
allowCommit - при установке значения false метод
isModified() данного источника всегда возвращает
false, а метод commit() ничего не делает. Таким
образом, изменения содержащихся в источнике
сущностей игнорируются. По умолчанию true, т.е.
изменения отслеживаются и могут быть сохранены.
datasourceClass - собственный класс реализации
источника данных, если необходим.
collectionDatasource - определяет источник данных,
содержащий коллекцию экземпляров.
Атрибуты collectionDatasource:
refreshMode - режим обновления источника, по
умолчанию ALWAYS. В режиме NEVER при вызове
refresh() источник не производит загрузку данных, а
только переходит в состояние Datasource.State.VALID,
оповещает слушателей и сортирует имеющиеся в нем
экземпляры. Режим NEVER удобен, если необходимо
программно заполнить CollectionDatasource
предварительно загруженными или созданными
сущностями. Например:
@Override
public void init(Map<String, Object> params) {
Set<Customer> entities = (Set<Customer>)
softDeletion - значение false отключает режим
мягкого удаления при загрузке сущностей, т.е. будут
загружены также и удаленные экземпляры. По
умолчанию true.
Элементы collectionDatasource:
query - запрос для загрузки сущностей
groupDatasource - полностью аналогичен
collectionDatasource, но создает реализацию
источника данных, пригодную для использования
совместно с компонентом GroupTable.
hierarchicalDatasource - аналогичен
collectionDatasource, и создает реализацию
источника данных, пригодную для использования
совместно с компонентами Tree и TreeTable.
Специфическим атрибутом является
hierarchyProperty, задающий имя атрибута сущности,
по которому строится иерархия.
Класс реализации источника выбирается неявно на
основе имени элемента XML и, как было сказано выше,
взаимного расположения элементов. Однако если
необходимо применить нестандартный источник данных,
его класс может быть явно указан в атрибуте
datasourceClass.
5.5.3.1.2. 5.5.3.1.2. Программное создание
При необходимости создать источник данных в Java коде
Set<Customer> entities = (Set<Customer>)
params.get("customers");
for (Customer entity : entities) {
customersDs.includeItem(entity);
}
customersDs.refresh();
}
При необходимости создать источник данных в Java коде
рекомендуется воспользоваться специальным классом
DsBuilder.
Экземпляр DsBuilder параметризуется цепочкой вызовов
его методов в стиле текучего (uent) интерфейса. Если
установлены параметры master и property, то в
результате будет создан NestedDatasource, в противном
случае - Datasource или CollectionDatasource.
Пример:
5.5.3.1.3. 5.5.3.1.3. Собственные классы реализации
Как правило, нестандартная реализация источника
данных требуется для изменения процесса загрузки
коллекции сущностей. При создании класса такого
источника рекомендуется унаследовать его от
CustomCollectionDatasource, либо от
CustomGroupDatasource или
CustomHierarchicalDatasource, и определить метод
getEntities().
Пример:
CollectionDatasource ds = new
DsBuilder(getDsContext())
.setJavaClass(Order.class)
.setViewName(View.LOCAL)
.setId("ordersDs")
.buildCollectionDatasource();
public class MyDatasource extends
CustomCollectionDatasource<SomeEntity, UUID> {
private SomeService someService =
AppBeans.get(SomeService.NAME);
Для создания экземпляра нестандартного источника
данных декларативным способом необходимо указать
класс в атрибуте datasourceClass элемента XML. При
программном создании через DsBuilder класс источника
указывается вызовом setDsClass() или в параметре
одного из методов build*().
5.5.3.2. 5.5.3.2. Запросы в
CollectionDatasourceImplCollectionDatasourceImpl
Класс CollectionDatasourceImpl и его наследники
GroupDatasourceImpl, HierarchicalDatasourceImpl
являются стандартной реализацией источников данных,
работающих с коллекциями независимых экземпляров
сущностей. Эти источники загружают данные через
DataManager, отправляя на MiddlewareMiddleware запрос на языке
JPQL. Далее рассматриваются особенности формирования
таких запросов.
5.5.3.2.1. 5.5.3.2.1. Возвращаемые значения
Запрос должен возвращать сущности того типа, который
указан при создании источника данных. Тип сущности при
декларативном создании указывается в атрибуте class
элемента XML, при создании через DsBuilder - в методе
setJavaClass() или setMetaClass().
AppBeans.get(SomeService.NAME);
@Override
protected Collection<SomeEntity>
getEntities(Map<String, Object> params) {
return someService.getEntities();
}
}
Например, запрос источника данных типа Customer может
выглядеть следующим образом:
или
Запрос не может возвращать агрегированные значения
или отдельные атрибуты, например:
5.5.3.2.2. 5.5.3.2.2. Параметры запроса
JPQL-запрос в источнике данных может содержать
параметры нескольких видов. Вид параметра
определяется по префиксу имени параметра. Префиксом
является часть имени до знака "$". Интерпретация имени
после "$" рассматривается ниже.
Префикс ds.
Значением параметра являются данные другого
источника данных, зарегистрированного в этом же
DsContext. Например:
select c from sales$Customer c
select o.customer from sales$Order o
select c.id, c.name from sales$Customer c
/* неверно
- возвращает отдельные поля, а не весь объект
Customer */
<collectionDatasource id="customersDs"
class="com.sample.sales.entity.Customer"
view="_local">
<query>
В данном случае параметром запроса источника данных
ordersDs будет текущий экземпляр сущности,
находящийся в источнике данных customersDs.
При использовании параметров с префиксом ds между
источниками данных автоматически создаются
зависимости, приводящие к обновлению источника если
меняется значение его параметра. В приведенном
примере если изменяется выбранный Покупатель,
автоматически обновляется список его Заказов.
Обратите внимание, что в примере запроса с
параметром левой частью оператора сравнения
является значение идентификатора o.customer.id, а
правой - экземпляр Customer, содержащийся в
источнике customersDs. Такое сравнение допустимо, так
как при выполнении запроса на MiddlewareMiddleware реализация
интерфейса Query, присваивая значения параметрам
запроса, автоматически подставляет ID сущности
вместо переданного экземпляра сущности.
В имени параметра после префикса и имени источника
может быть также указан путь по графу сущностей к
атрибуту, из которого нужно взять значение, например:
<![CDATA[select c from sales$Customer c]]>
</query>
</collectionDatasource>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order" view="_local">
<query>
<![CDATA[select o from sales$Order o where
o.customer.id = :ds$customersDs]]>
</query>
</collectionDatasource>
или
Префикс custom.
Значение параметра будет взято из объекта
Map<String, Object>, переданного в метод refresh()
источника данных. Например:
Приведение экземпляра при необходимости к его
идентификатору осуществляется аналогично
параметрам с префиксом ds. Путь по графу сущностей в
имени параметра в данном случае не поддерживается.
Префикс param.
<query>
<![CDATA[select o from sales$Order o where
o.customer.id = :ds$customersDs.id]]>
</query>
<query>
<![CDATA[select o from sales$Order o where
o.tagName = :ds$customersDs.group.tagName]]>
</query>
<collectionDatasource id="ordersDs"
class="com.sample.sales.entity.Order" view="_local">
<query>
<![CDATA[select o from sales$Order o where
o.number = :custom$number]]>
</query>
</collectionDatasource>
ordersDs.refresh(ParamsMap.of("number", "1"));
Значение параметра будет взято из объекта
Map<String, Object>, переданного при открытии
экрана в метод init() контроллера. Например:
Приведение экземпляра при необходимости к его
идентификатору осуществляется аналогично
параметрам с префиксом ds. Поддерживается путь к
атрибуту по графу сущностей в имени параметра.
Префикс component.
Значением параметра будет текущее значение
визуального компонента, путь к которому указан в
имени параметра. Например:
Путь к компоненту должен включать все вложенные
фреймы.
Приведение экземпляра при необходимости к его
идентификатору аналогично параметрам ds.
<query>
<![CDATA[select e from sales$Order e where
e.customer = :param$customer]]>
</query>
openWindow("sales$Order.lookup",
WindowManager.OpenType.DIALOG,
ParamsMap.of("customer",
customersTable.getSingleSelected()));
<query>
<![CDATA[select o from sales$Order o where
o.number = :component$filter.orderNumberField]]>
</query>
идентификатору аналогично параметрам ds.
Поддерживается путь к атрибуту по графу сущностей в
имени параметра как продолжение пути к компоненту.
При изменении значения компонента источник
данных автоматически не обновляется.
Префикс session.
Значением параметра будет значение атрибута
пользовательской сессии, указанного в имени
параметра.
Значение извлекается методом
UserSession.getAttribute(), поэтому поддерживаются
также предопределенные имена атрибутов сессии:
userId - ID текущего зарегистрированного или
замещенного пользователя;
userLogin - логин текущего зарегистрированного или
замещенного пользователя в нижнем регистре.
Пример:
Приведение экземпляра при необходимости к его
идентификатору аналогично параметрам ds. Путь по
графу сущностей в имени параметра в данном случае
не поддерживается.
Если значение параметра не найдено по правилам,
задаваемым префиксом, для данного параметра
устанавливается значение null. То есть если,
например, в запросе указан параметр с именем
param$some_name, а в мэп параметров экрана нет
<query>
<![CDATA[select o from sales$Order o where
o.createdBy = :session$userLogin]]>
</query>
param$some_name, а в мэп параметров экрана нет
ключа some_name, то для параметра param$some_name
устанавливается значение null.
5.5.3.2.3. 5.5.3.2.3. Фильтр запроса
Запрос источника данных может быть модифицирован во
время работы приложения, в зависимости от вводимых
пользователем условий, что позволяет эффективно
фильтровать данные на уровне выборки из БД.
Простейший способ обеспечения такой возможности -
подключение к источнику данных специального
визуального компонента Filter.
Если по какой-то причине применение универсального
фильтра нежелательно, можно встроить в текст запроса
специальную разметку на XML, позволяющую
сформировать итоговый запрос в зависимости от
значений, введенных пользователем в произвольные
визуальные компоненты экрана.
В таком фильтре могут быть использованы следующие
элементы:
filter - корневой элемент фильтра. Может
непосредственно содержать только одно условие.
and, or - логические условия, могут содержать любое
количество других условий и предложений.
c - предложение на JPQL, которое добавляется в секцию
where. Содержит только текст и опционально атрибут
join, значение которого будет добавлено в
соответствующее место запроса, если добавляется
данное предложение where.
Условия и предложения добавляются в итоговый запрос,
только если присутствующие внутри них параметры
получили значения, т.е. не равны null.
В фильтрах запросов можно использовать только
custom, param и component параметры. Параметры ds и
session не будут работать.
Пример:
В данном случае если в метод refresh() источника
данных переданы параметры state и initiator, а в
визуальном компоненте barCodeFilterField установлено
некоторое значение, то итоговый запрос примет вид:
<query>
<![CDATA[select distinct d from app$GeneralDoc
d]]>
<filter>
<or>
<and>
<c join=", app$DocRole dr">dr.doc.id
= d.id and d.processState = :custom$state</c>
<c>d.barCode like
:component$barCodeFilterField</c>
</and>
<c join=", app$DocRole dr">dr.doc.id =
d.id and dr.user.id = :custom$initiator</c>
</or>
</filter>
</query>
select distinct d from app$GeneralDoc d, app$DocRole
dr
where
(
(dr.doc.id = d.id and d.processState =
:custom$state)
Если же, к примеру, компонент barCodeFilterField пуст, а
в refresh() передан только параметр initiator, то
запрос получится следующим:
5.5.3.2.4. 5.5.3.2.4. Поиск подстроки без учета регистра
В источниках данных можно использовать особенность
выполнения JPQL-запросов, описанную для интерфейса
Query уровня MiddlewareMiddleware: для удобного формирования
условия поиска без учета регистра символов и по любой
части строки можно использовать префикс (?i). Однако,
в связи с тем, что значение параметра обычно передается
неявно, имеются следующие отличия:
Префикс (?i) нужно указывать не в значении, а перед
именем параметра.
Значение параметра будет автоматически переведено
в нижний регистр.
Если в значении параметра отсутствуют символы %, то
они будут добавлены в начало и конец.
Для примера рассмотрим обработку следующего запроса:
and
(d.barCode like :component$barCodeFilterField)
)
or
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
select distinct d from app$GeneralDoc d, app$DocRole
dr
where
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
select c from sales$Customer c where c.name like :(?
В данном случае значение параметра, взятое из
компонента customerNameField, будет переведено в
нижний регистр и обрамлено символами %, а затем в базе
данных будет выполнен SQL запрос с условием вида
lower(C.NAME) like ?
Следует иметь в виду, что при таком поиске индекс,
созданный в БД по полю NAME, не используется.
5.5.3.3. Value Datasources5.5.3.3. Value Datasources
Value datasources позволяют выполнять запросы,
возвращающие скалярные и агрегатные значения.
Например, следующим запросом можно загрузить
некоторую агрегатную статистику по покупателям:
Value datasources работают с сущностями особого типа:
KeyValueEntity. Такая сущность может содержать
произвольный набор атрибутов, задаваемый во время
работы приложения. Так, в примере выше, экземпляры
KeyValueEntity будут содержать два атрибута: первый
типа Customer, второй типа BigDecimal.
Реализации value datasources унаследованы от других
широко используемых классов источников данных и
дополнительно реализуют специфический интерфейс
ValueDatasource. На диаграмме ниже изображены
реализации value datasources и их базовые классы:
select c from sales$Customer c where c.name like :(?
i)component$customerNameField
select o.customer, sum(o.amount) from demo$Order o
group by o.customer
Интерфейс ValueDatasource содержит следующие
методы:
addProperty() - так как источник данных может
содержать произвольный набор атрибутов, с помощью
данного метода необходимо указать, какие атрибуты
ожидаются. Он принимает имя атрибута и его тип в
виде Datatype или Java-класса. В последнем случае класс
должен быть либо сущностью, либо классом,
поддерживаемым одним из типов данных (datatypes).
setIdName() - опциональный метод, позволяющий
назначить один из атрибутов идентификатором
сущности. Это означает, что экземпляры
KeyValueEntity, содержащиеся в данном источнике
данных будут иметь идентификаторы, получаемые из
данного атрибута. В противном случае, экземпляры
KeyValueEntity получают случайно сгенерированные
UUIDs.
getMetaClass() возвращает динамическую реализацию
интерфейса MetaClass, которая представляет текущую
схему экземпляров KeyValueEntity, заданную вызовами
метода addProperty().
Value datasources могут быть заданы декларативно в XML-
дескрипторе. Существует три XML-элемента,
соответствующих классам реализации:
valueCollectionDatasource
valueGroupDatasource
valueGroupDatasource
valueHierarchicalDatasource
XML описание value datasource должно содержать элемент
properties, который задает атрибуты KeyValueEntity,
содержащихся в источнике данных (см. метод
addProperty() выше). Порядок элементов property
должен соответствовать порядку колонок в
результирующем наборе, возвращаемом запросом.
Например, в следующем определении атрибут customer
получит значение из колонки o.customer, а атрибут sum из
колонки sum(o.amount):
Value datasources предназначены только для чтения
данных, так как сущность KeyValueEntity является
неперсистентной и не может быть сохранена
стандартным механизмом работы с БД.
5.5.3.4. 5.5.3.4. Слушатели источников
данных
<dsContext>
<valueCollectionDatasource id="salesDs">
<query>
<![CDATA[select o.customer, sum(o.amount)
from demo$Order o group by o.customer]]>
</query>
<properties>
<property
class="com.company.demo.entity.Customer"
name="customer"/>
<property datatype="decimal" name="sum"/>
</properties>
</valueCollectionDatasource>
</dsContext>
данных
Слушатели источников данных (datasource listeners)
позволяют получать оповещения об изменении состояния
источников данных и экземпляров сущностей, в них
находящихся.
Существует четыре типа слушателей. Три из них:
ItemPropertyChangeListener, ItemChangeListener и
StateChangeListener определены в интерфейсе
Datasource и могут быть использованы в любых
источниках данных. CollectionChangeListener
определен в интерфейсе CollectionDatasource и может
быть использован только в источниках данных,
работающих с коллекциями сущностей.
Пример использования слушателей источников данных:
public class EmployeeBrowse extends AbstractLookup {
private Logger log =
LoggerFactory.getLogger(EmployeeBrowse.class);
@Inject
private CollectionDatasource<Employee, UUID>
employeesDs;
@Override
public void init(Map<String, Object> params) {
employeesDs.addItemPropertyChangeListener(event -> {
log.info("Property {} of {} has been
changed from {} to {}",
event.getProperty(),
event.getItem(), event.getPrevValue(),
event.getValue());
});
Интерфейсы слушателей описаны ниже.
ItemPropertyChangeListener добавляется с помощью
метода Datasource.addItemPropertyChangeListener().
Слушатель вызывается, если изменилось значение
какого-либо атрибута сущности, находящейся в данный
момент в источнике. Через объект события можно
получить сам измененный экземпляр, имя измененного
атрибута, старое и новое значение.
Слушатель ItemPropertyChangeListener можно
использовать для действий в ответ на изменение
employeesDs.addStateChangeListener(event -> {
log.info("State of {} has been changed
from {} to {}",
event.getDs(),
event.getPrevState(), event.getState());
});
employeesDs.addItemChangeListener(event -> {
log.info("Datasource {} item has been
changed from {} to {}",
event.getDs(),
event.getPrevItem(), event.getItem());
});
employeesDs.addCollectionChangeListener(event
-> {
log.info("Datasource {} content has been
changed due to {}",
event.getDs(),
event.getOperation());
});
}
}
использовать для действий в ответ на изменение
пользователем сущности из UI, то есть редактирования
полей ввода.
ItemChangeListener добавляется с помощью метода
Datasource.addItemChangeListener(). Он вызывается
при смене выбранного экземпляра, возвращаемого
методом getItem().
Для Datasource это происходит при установке другого
экземпляра (или null) методом setItem().
Для CollectionDatasource данный слушатель
вызывается, когда в связанном визуальном компоненте
меняется выделенный элемент. Например, это может
быть выделенная строка таблицы, элемент дерева, или
выделенный элемент выпадающего списка.
StateChangeListener добавляется с помощью метода
Datasource.addStateChangeListener(). Он вызывается
при изменении состояния источника данных. Источник
данных может находиться в одном из трех состояний,
соответствующих перечислению Datasource.State:
NOT_INITIALIZED - источник только что создан.
INVALID - создан весь DsContext, к которому относится
данный источник.
VALID - источник данных в рабочем состоянии:
Datasource содержит экземпляр сущности или null,
CollectionDatasource - коллекцию экземпляров или
пустую коллекцию.
Получение оповещения об изменении состояния
источника может быть актуально для сложных
редакторов, состоящих из нескольких фреймов, где
сложно отследить момент проставления
редактируемой сущности в источник. В этом случае
можно использовать слушатель StateChangeListener
для отложенной инициализации некоторых элементов
экрана:
экрана:
CollectionChangeListener добавляется с помощью
метода
CollectionDatasource.addCollectionChangeListener(
). Слушатель вызывается при изменении коллекции
сущностей, хранящейся в источнике данных. Объект
события имеет метод getOperation(), возвращающий
значение типа CollectionDatasource.Operation:
REFRESH, CLEAR, ADD, REMOVE, UPDATE. Этот тип указывает
операцию, которая привела к изменению коллекции.
5.5.3.5. DsContext5.5.3.5. DsContext
Все созданные декларативно источники данных
регистрируются в объекте DsContext экрана. Ссылку на
DsContext можно получить методом getDsContext()
контроллера экрана, либо инжекцией в поле класса.
DsContext решает следующие задачи:
1. Позволяет организовать зависимости между
источниками данных, когда при навигации по одному
источнику (т.е. при изменении "текущего" экземпляра
методом setItem()) обновляется связанный источник.
Такие зависимости дают возможность в экранах легко
организовывать master-detail связи между визуальными
компонентами.
Зависимости между источниками организуются с
помощью параметров запросов с префиксом ds$.
employeesDs.addStateChangeListener(event -> {
if (event.getState() == Datasource.State.VALID)
initDataTypeColumn();
});
2. Позволяет собрать все измененные экземпляры
сущностей и отправить их на MiddlewareMiddleware в одном вызове
DataManager.commit(), т.е. сохранить в базе данных в
одной транзакции.
В качестве примера предположим, что некоторый экран
позволяет редактировать экземпляр сущности Order и
коллекцию принадлежащих ему экземпляров
OrderLine. Экземпляр Order находится в Datasource,
коллекция OrderLine - во вложенном
CollectionDatasource, созданном по атрибуту
Order.lines. Допустим, пользователь изменил какой-то
атрибут Order и создал новый экземпляр OrderLine.
Тогда при коммите экрана в DataManager будут
одновременно отправлены два экземпляра -
измененный Order и новый OrderLine. Далее, они
вместе попадут в один персистентный контекст и при
коммите транзакции сохранятся в БД. Разумеется,
экземпляр OrderLine содержится также в коллекции
Order.lines, но если не передавать его в
персистентный контекст независимо, то потребуется
установка каскадности сохранения между Order и
OrderLines на уровне ORM. Жесткие отношения
каскадности на уровне ORM иногда вызывают
нежелательные последствия в неожиданных местах,
поэтому лучше их избегать, что и обеспечивает
описываемый механизм DsContext.
В результате коммита DsContext получает от MiddlewareMiddleware
набор сохраненных экземпляров (в случае
оптимистической блокировки у них, как минимум,
увеличено значение атрибута version), и устанавливает
эти экземпляры в источниках данных взамен
устаревших. Это позволяет сразу после коммита
работать со свежими экземплярами без необходимости
лишнего обновления источников данных, связанного с
лишнего обновления источников данных, связанного с
запросами к MiddlewareMiddleware и базе данных.
3. Объявляет два слушателя: BeforeCommitListener и
AfterCommitListener, позволяющие получать
оповещения перед коммитом измененных экземпляров
и после него. Перед коммитом можно дополнить
коллекцию отправляемых в DataManager на Middleware
экземпляров, тем самым обеспечив сохранение в той же
транзакции произвольных сущностей. После коммита
можно получить коллекцию вернувшихся из
DataManager сохраненных экземпляров.
Данный механизм необходим, если некоторые
сущности, с которыми работает экран, находятся не под
управлением источников данных, а создаются и
изменяются непосредственно в коде контроллера.
Например, визуальный компонент FileUploadField
после загрузки файла создает новый экземпляр
сущности FileDescriptor, который можно сохранить
вместе с другими сущностями экрана именно таким
способом - добавив в CommitContext в слушателе
BeforeCommitListener.
В следующем примере новый экземпляр Customer будет
отправлен на MiddlewareMiddleware и сохранен в БД вместе с
остальными измененными сущностями экрана при его
коммите:
protected Customer customer;
protected void createNewCustomer() {
customer = metadata.create(Customer.class);
customer.setName("John Doe");
}
@Override
public void init(Map<String, Object> params) {
5.5.3.6. DataSupplier5.5.3.6. DataSupplier
DataSupplier - интерфейс, через который источники
данных обращаются к Middleware для загрузки и
сохранения сущностей. Его стандартная реализация
просто делегирует выполнение DataManager. Экран может
задать свою реализацию интерфейса DataSupplier в
атрибуте dataSupplier элемента window.
Ссылку на DataSupplier можно получить либо инжекцией
в контроллер экрана, либо через экземпляры DsContext
или Datasource. В обоих случаях возвращается или
стандартная, или собственная реализация интерфейса
(если таковая определена).
5.5.4. 5.5.4. Действия. Интерфейс Action Action
Action − интерфейс, абстрагирующий действие (другими
словами, некоторую функцию) от визуального
компонента. Он особенно полезен в случаях, когда одно и
то же действие может быть вызвано из разных
визуальных компонентов. Кроме того, данный интерфейс
позволяет снабдить действие дополнительными
свойствами, такими как название, признаки доступности и
видимости, и другими.
Рассмотрим методы интерфейса Action:
public void init(Map<String, Object> params) {
getDsContext().addBeforeCommitListener(context -
> {
if (customer != null)
context.getCommitInstances().add(customer);
}
}
Рассмотрим методы интерфейса Action:
actionPerform() - вызывается визуальным
компонентом, связанным с данным действием. В метод
передается экземпляр вызвавшего компонента.
getId() - возвращает идентификатор данного
действия. Идентификатор обычно устанавливается
конструктором класса, реализующего Action, и не
меняется на протяжении жизни созданного объекта
действия.
методы получения и установки свойств caption,
description, shortcut, icon, enabled, visible. Все эти
свойства обычно используется связанными
визуальными компонентами для установки
собственных одноименных свойств.
addPropertyChangeListener(),
removePropertyChangeListener() - подключение
слушателей, реагирующих на изменение
вышеупомянутых свойств. Слушатель получает
уведомление типа java.beans.PropertyChangeEvent, в
котором содержится имя измененного свойства, его
старое и новое значение.
refreshState() - метод, который может быть
реализован в конкретном классе действия для
инициализации вышеупомянутых свойств в
соответствии с какими-либо внешними факторами,
например правами пользователя. Вызывается обычно в
конструкторах имплементирующих классов или из
связанных визуальных компонентов.
addOwner(), removeOwner(), getOwner(), getOwners() -
методы для управления связью действия с визуальными
компонентами.
Для реализации действий рекомендуется использовать
декларативное создание действий, либо наследоваться от
класса AbstractAction. Кроме того, существует набор
класса AbstractAction. Кроме того, существует набор
стандартных действий, применимых для работы с
таблицами и компонентами выбора. От стандартных
действий также можно наследоваться для модификации
их поведения или перехвата событий.
Визуальные компоненты, связанные с действием, могут
быть двух типов:
Визуальный компонент, содержащий одно действие,
реализует интерфейс Component.ActionOwner. Это Button
и LinkButton.
Связь компонента с действием осуществляется путем
вызова метода ActionOwner.setAction() компонента. В
этот момент компонент заменяет свои свойства на
соответствующие свойства действия (подробнее см.
описание компонентов).
Визуальный компонент, содержащий несколько
действий, реализует интерфейс
Component.ActionsHolder. Это Window, Frame, Table и ее
наследники, Tree, PopupButton, PickerField, LookupPickerField.
Действия добавляются компоненту вызовом метода
ActionsHolder.addAction(). Реализация этого метода в
компоненте проверяет, нет ли уже в нем действия с
таким же идентификатором. Если есть, то имеющееся
действие будет заменено на новое переданное.
Поэтому можно, например, декларировать стандартное
действие в дескрипторе экрана, а затем в контроллере
создать новое с переопределенными методами и
добавить компоненту.
5.5.4.1. 5.5.4.1. Декларативное создание
действий
В XML-дескрипторе экрана для любого компонента,
В XML-дескрипторе экрана для любого компонента,
реализующего интерфейс Component.ActionsHolder, в
том числе для всего экрана или фрейма, может быть
задан набор действий. Делается это в элементе actions,
который содержит вложенные элементы action.
Элемент action может иметь следующие атрибуты:
id − идентификатор, должен быть уникален в рамках
данного компонента ActionsHolder.
caption - название действия.
description - описание действия.
enable - признак доступности действия (true / false).
icon - значок действия.
invoke - имя вызываемого метода контроллера. Метод
должен быть public, не возвращать результата и либо
не иметь аргументов, либо иметь один аргумент типа
Component. Если метод имеет аргумент Component, то
при вызове в него будет передан экземпляр
визуального компонента, запустившего данное
действие.
shortcut - комбинация клавиш для вызова.
Комбинации можно жёстко задавать в XML-
дескрипторе. Возможные модификаторы - ALT, CTRL,
SHIFT - отделяются символом "-". Например:
Для большей гибкости можно использовать готовые
псевдонимы комбинаций из списка ниже, к примеру:
<action id="create" shortcut="ALT-N"/>
<action id="edit"
shortcut="${TABLE_EDIT_SHORTCUT}"/>
TABLE_EDIT_SHORTCUT
COMMIT_SHORTCUT
CLOSE_SHORTCUT
FILTER_APPLY_SHORTCUT
FILTER_SELECT_SHORTCUT
NEXT_TAB_SHORTCUT
PREVIOUS_TAB_SHORTCUT
PICKER_LOOKUP_SHORTCUT
PICKER_OPEN_SHORTCUT
PICKER_CLEAR_SHORTCUT
Кроме того, есть возможность задавать комбинацию с
помощью полного имени интерфейса Config и имени
метода, возвращающего нужную комбинацию:
visible - признак видимости действия (true / false).
Рассмотрим примеры декларативного объявления
действий.
Объявление действий на уровне экрана:
<action id="remove"
shortcut="${com.haulmont.cuba.client.ClientConfig#ge
tTableRemoveShortcut}"/>
<window ...>
<dsContext/>
<actions>
<action id="sayHelloAction"
caption="msg://sayHello" shortcut="ALT-T"
invoke="sayHello"/>
</actions>
Здесь объявляется действие с идентификатором
sayHelloAction и названием из пакета сообщений. С этим
действием связывается кнопка, заголовок которой будет
установлен в название действия. Действие вызовет метод
sayHello() контроллера при нажатии на кнопку, а также
при нажатии комбинации клавиш ALT-TALT-T, если в данный
момент экран принимает фокус ввода.
Объявление действий для PopupButton:
Объявление действий для Table:
<layout>
<button action="sayHelloAction"/>
</layout>
</window>
// controller
public void sayHello(Component component) {
showNotification("Hello!", NotificationType.TRAY);
}
<popupButton caption="Say something">
<actions>
<action id="helloAction" caption="Say hello"
invoke="sayHello"/>
<action id="goodbyeAction" caption="Say goodbye"
invoke="sayGoodbye"/>
</actions>
</popupButton>
<table id="usersTable" width="100%">
Здесь помимо стандартных действий таблицы create и
edit объявлены действия copy и changePassw,
вызывающие соответствующие методы контроллера. Для
этих действий указан также атрибут
trackSelection="true", в результате чего действие и
связанная с ним кнопка становятся недоступными, если в
таблице не выбрана ни одна строка. Это удобно, если
действие предназначено для выполнения над текущей
выбранной строкой таблицы.
<actions>
<action id="create"/>
<action id="edit"/>
<action id="copy" caption="msg://copy"
icon="icons/copy.png"
invoke="copy" trackSelection="true"/>
<action id="changePassw"
caption="msg://changePassw" icon="icons/change-
pass.png"
invoke="changePassword"
trackSelection="true"/>
</actions>
<buttonsPanel>
<button action="usersTable.create"/>
<button action="usersTable.edit"/>
<button action="usersTable.copy"/>
<button action="usersTable.changePassw"/>
</buttonsPanel>
<rowsCount/>
<columns>
<column id="login"/>
...
</columns>
<rows datasource="usersDs"/>
</table>
Для действий create и edit можно указать
дополнительный атрибут openType для указания режима
открытия экрана редактирования, как описано для
метода setOpenType() класса CreateAction.
Объявление действий для PickerField:
В данном примере для компонента PickerField
объявлено стандартное действие lookup и действие show,
вызывающее метод showColour() контроллера. Так как в
кнопках PickerField, отображающих действия,
используются значки, а не надписи, атрибут caption явно
установлен в пустую строку, иначе названием действия и
заголовком кнопки стал бы идентификатор действия.
Атрибут description позволяет отображать
всплывающую подсказку при наведении мыши на кнопку
действия.
Ссылки на любые декларативно объявленные действия
можно получить в контроллере экрана либо
непосредственно путем инжекции, либо из компонентов,
реализующих интерфейс Component.ActionsHolder. Это
может понадобиться для программной установки свойств
действия. Например:
<pickerField id="colourField" datasource="carDs"
property="colour"/>
<actions>
<action id="lookup"/>
<action id="show" icon="icons/show.png"
invoke="showColour" caption=""
description="Show colour"/>
</actions>
</pickerField>
5.5.4.2. 5.5.4.2. Стандартные действия
Стандартные действия - это классы, имплементирующие
интерфейс Action, и предназначенные для решения
типовых задач, таких как вызов экрана редактирования
для сущности, выбранной в таблице. Стандартные
действия имеют строго определенные идентификаторы,
поэтому для декларативного объявления некоторого
стандартного действия достаточно указать его
идентификатор.
Существует два вида стандартных действий:
Действия с коллекцией сущностей, отображаемой в
таблице или дереве.
@Named("carsTable.create")
private CreateAction createAction;
@Named("carsTable.copy")
private Action copyAction;
@Inject
private PickerField colourField;
@Override
public void init(Map<String, Object> params) {
Map<String, Object> values = new HashMap<>();
values.put("type", CarType.PASSENGER);
createAction.setInitialValues(values);
copyAction.setEnabled(false);
Action showAction = colourField.getAction("show");
showAction.setEnabled(false);
}
таблице или дереве.
Действия поля выбора экземпляра сущности.
5.5.4.2.1. 5.5.4.2.1. Стандартные действия с коллекцией
Для наследников ListComponent (это Table, GroupTable,
TreeTable и Tree) набор стандартных действий определен в
перечислении ListActionType, классы их реализации
находятся в пакете
com.haulmont.cuba.gui.components.actions.
Пример использования стандартных действий в таблице:
Рассмотрим их подробнее.
CreateActionCreateAction
<table id="usersTable" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
<action id="refresh"/>
</actions>
<buttonsPanel>
<button action="usersTable.create"/>
<button action="usersTable.edit"/>
<button action="usersTable.remove"/>
<button action="usersTable.refresh"/>
</buttonsPanel>
<rowsCount/>
<columns>
<column id="login"/>
...
</columns>
<rows datasource="usersDs"/>
</table>
CreateAction - действие с идентификатором createcreate.
Предназначено для создания нового экземпляра
сущности и открытия экрана редактирования для этого
экземпляра. Если экран редактирования успешно
закоммитил новый экземпляр в базу данных, то
CreateAction добавляет этот новый экземпляр в источник
данных таблицы и делает его выбранным.
В классе CreateAction определены следующие
специфические методы:
setOpenType() - позволяет задать режим открытия
экрана редактирования новой сущности. По умолчанию
экран открывается в режиме THIS_TAB.
Так как довольно часто требуется открывать экраны
редактирования в другом режиме (как правило, DIALOG),
при декларативном создании действия create в
элементе action можно указать атрибут openType с
нужным значением. Это избавляет от необходимости
получать ссылку на действие в контроллере и
программно устанавливать данное свойство. Например:
setWindowId() - позволяет задать идентификатор
экрана редактирования сущности. По умолчанию
используется экран {имя_сущности}.edit, например
sales$Customer.edit.
setWindowParams() - позволяет задать параметры
экрана редактирования, передаваемые в его метод
init(). Далее эти параметры можно использовать
напрямую в запросе источника данных через префикс
param$ или инжектировать в контроллер экрана с
<table id="usersTable">
<actions>
<action id="create" openType="DIALOG"/>
param$ или инжектировать в контроллер экрана с
помощью аннотации @WindowParam.
setWindowParamsSupplier() отличается от
setWindowParams() тем, что позволяет получить
значения параметров непосредственно перед вызовом
действия. Полученные параметры объединяются с
предоставленными через setWindowParams() и могут
переопределять их. Например:
setInitialValues() - позволяет задать начальные
значения атрибутов создаваемой сущности. Принимает
объект Map, в котором ключами являются имена
атрибутов, а значениями - значения атрибутов.
Например:
Пример использования setInitialValues() приведен
также в разделе рецептов разработки.
setInitialValuesSupplier() отличается от
setInitialValues() тем, что позволяет получить
значения параметров непосредственно перед вызовом
действия. Полученные параметры объединяются с
предоставленными через setInitialValues() и могут
createAction.setWindowParamsSupplier(() -> {
Customer customer =
metadata.create(Customer.class);
customer.setCategory(
/* some value dependent on
the current state of the screen */
);
return ParamsMap.of("customer", customer);
});
Map<String, Object> values = new HashMap<>();
values.put("type", CarType.PASSENGER);
carCreateAction.setInitialValues(values);
предоставленными через setInitialValues() и могут
переопределять их. Например:
afterCommit() - вызывается действием после того, как
экран редактирования успешно закоммитил новую
сущность и был закрыт. Данный метод не имеет
реализации и может быть переопределен в
наследниках для реакции на это событие.
setAfterCommitHandler() - позволяет задать
обработчик, который будет вызван после того, как экран
редактирования успешно закоммитил новую сущность
и был закрыт. Данный обработчик можно использовать
вместо переопределения метода afterCommit(), тем
самым избавившись от необходимости создания
наследника действия. Например:
carCreateAction.setInitialValuesSupplier(() ->
ParamsMap.of("type",
/* value depends on the
current state of the screen */
));
@Named("customersTable.create")
private CreateAction customersTableCreate;
@Override
public void init(Map<String, Object> params) {
customersTableCreate.setAfterCommitHandler(new
CreateAction.AfterCommitHandler() {
@Override
public void handle(Entity entity) {
showNotification("Committed",
NotificationType.HUMANIZED);
}
});
}
afterWindowClosed() - вызывается действием в
последнюю очередь после закрытия экрана
редактирования, независимо от того, была ли
закоммичена новая сущность или нет. Данный метод не
имеет реализации и может быть переопределен в
наследниках для реакции на это событие.
setAfterWindowClosedHandler() - позволяет задать
обработчик, который будет вызван после закрытия
экрана редактирования, независимо от того, была ли
закоммичена новая сущность или нет. Данный
обработчик можно использовать вместо
переопределения метода afterWindowClosed(), тем
самым избавившись от необходимости создания
наследника действия.
EditActionEditAction
EditAction - действие с идентификатором editedit. Открывает
экран редактирования для выбранного экземпляра
сущности. Если экран редактирования успешно
закоммитил экземпляр в базу данных, то EditAction
обновляет этот экземпляр в источнике данных таблицы.
В классе EditAction определены следующие
специфические методы:
setOpenType() - позволяет задать режим открытия
экрана редактирования сущности. По умолчанию экран
открывается в режиме THIS_TAB.
Так как довольно часто требуется открывать экраны
редактирования в другом режиме (как правило, DIALOG),
при декларативном создании действия edit в элементе
action можно указать атрибут openType с нужным
значением. Это избавляет от необходимости получать
ссылку на действие в контроллере и программно
устанавливать данное свойство. Например:
setWindowId() - позволяет задать идентификатор
экрана редактирования сущности. По умолчанию
используется экран {имя_сущности}.edit, например
sales$Customer.edit.
setWindowParams() - позволяет задать параметры
экрана редактирования, передаваемые в его метод
init(). Далее эти параметры можно использовать
напрямую в запросе источника данных через префикс
param$ или инжектировать в контроллер экрана с
помощью аннотации @WindowParam.
setWindowParamsSupplier() отличается от
setWindowParams() тем, что позволяет получить
значения параметров непосредственно перед вызовом
действия. Полученные параметры объединяются с
предоставленными через setWindowParams() и могут
переопределять их. Например:
afterCommit() - вызывается действием после того, как
экран редактирования успешно закоммитил сущность и
был закрыт. Данный метод не имеет реализации и
может быть переопределен в наследниках для реакции
на это событие.
setAfterCommitHandler() - позволяет задать
обработчик, который будет вызван после того, как экран
<table id="usersTable">
<actions>
<action id="edit" openType="DIALOG"/>
customersTableEdit.setWindowParamsSupplier(() ->
ParamsMap.of("category",
/* some value dependent
on the current state of the screen */
));
обработчик, который будет вызван после того, как экран
редактирования успешно закоммитил новую сущность
и был закрыт. Данный обработчик можно использовать
вместо переопределения метода afterCommit(), тем
самым избавившись от необходимости создания
наследника действия. Например:
afterWindowClosed() - вызывается действием в
последнюю очередь после закрытия экрана
редактирования, независимо от того, была ли
закоммичена редактируемая сущность. Данный метод
не имеет реализации и может быть переопределен в
наследниках для реакции на это событие.
setAfterWindowClosedHandler() - позволяет задать
обработчик, который будет вызван после закрытия
экрана редактирования, независимо от того, была ли
закоммичена новая сущность или нет. Данный
обработчик можно использовать вместо
переопределения метода afterWindowClosed(), тем
самым избавившись от необходимости создания
@Named("customersTable.edit")
private EditAction customersTableEdit;
@Override
public void init(Map<String, Object> params) {
customersTableEdit.setAfterCommitHandler(new
EditAction.AfterCommitHandler() {
@Override
public void handle(Entity entity) {
showNotification("Committed",
NotificationType.HUMANIZED);
}
});
}
самым избавившись от необходимости создания
наследника действия.
getBulkEditorIntegration() позволяет настроить
массовое редактирование строк таблицы, для этого
атрибут multiselect таблицы должен иметь значение
true. Компонент BulkEditor будет открыт, если при
вызове EditAction в таблице выделено более одной
строки.
Возвращаемый экземпляр BulkEditorIntegration
можно изменить с помощью следующих методов:
setOpenType(),
setExcludePropertiesRegex(),
setFieldValidators(),
setModelValidators(),
setAfterEditCloseHandler().
RemoveActionRemoveAction
RemoveAction - действие с идентификатором removeremove.
Предназначено для удаления выбранного экземпляра
сущности.
В классе RemoveAction определены следующие
@Named("clientsTable.edit")
private EditAction clientsTableEdit;
@Override
public void init(Map<String, Object> params) {
super.init(params);
clientsTableEdit.getBulkEditorIntegration()
.setEnabled(true)
.setOpenType(WindowManager.OpenType.DIALOG);
}
специфические методы:
setAutocommit() - позволяет управлять моментом
удаления сущности из базы данных. По умолчанию
после срабатывания действия и удаления сущности из
источника данных у источника вызывается метод
commit(), в результате чего сущность удаляется из базы
данных. Cвойство autocommit можно установить в false
либо методом setAutocommit(), либо соответствующим
параметром конструктора. В результате после
удаления сущности из источника данных для
подтверждения удаления потребуется явно вызвать
метод commit() источника данных.
Значение autocommit не влияет на работу источников
данных в режиме Datasource.CommitMode.PARENT, то
есть тех, которые обеспечивают редактирование
композиционных сущностей.
setConfirmationMessage() - позволяет задать текст
сообщения в диалоге подтверждения удаления.
setConfirmationTitle() - позволяет задать заголовок
диалога подтверждения удаления.
afterRemove() - вызывается действием после
успешного удаления сущности. Данный метод не имеет
реализации и может быть переопределен в
наследниках для реакции на это событие.
setAfterRemoveHandler() позволяет задать обработчик,
который будет вызван после успешного удаления
сущности. Данный обработчик можно использовать
вместо переопределения метода afterWindowClosed(),
тем самым избавившись от необходимости создания
наследника действия. Например:
@Named("customersTable.remove")
private RemoveAction customersTableRemove;
RefreshActionRefreshAction
RefreshAction - действие с идентификатором refreshrefresh.
Предназначено для обновления (перезагрузки) коллекции
сущностей. При срабатывании вызывает метод refresh()
источника данных, с которым связан компонент.
В классе RefreshAction определены следующие
специфические методы:
setRefreshParams() - позволяет задать параметры,
передаваемые в метод
CollectionDatasource.refresh(), для использования
внутри запроса. По умолчанию никакие параметры не
передаются.
setRefreshParamsSupplier() отличается от
setRefreshParams() тем, что позволяет получить
значения параметров непосредственно перед вызовом
действия. Полученные параметры объединяются с
предоставленными через setRefreshParams() и могут
переопределять их. Например:
private RemoveAction customersTableRemove;
@Override
public void init(Map<String, Object> params) {
customersTableRemove.setAfterRemoveHandler(new
RemoveAction.AfterRemoveHandler() {
@Override
public void handle(Set removedItems) {
showNotification("Removed",
NotificationType.HUMANIZED);
}
});
}
customersTableRefresh.setRefreshParamsSupplier(() ->
AddActionAddAction
AddAction - действие с идентификатором addadd.
Предназначено для выбора существующего экземпляра
сущности и добавления его в коллекцию. При
срабатывании открывает экран выбора сущностей.
В классе AddAction определены следующие
специфические методы:
setOpenType() - позволяет задать режим открытия
экрана выбора сущности. По умолчанию экран
открывается в режиме THIS_TAB.
Так как довольно часто требуется открывать экраны
выбора в другом режиме (как правило, DIALOG), при
декларативном создании действия add в элементе
action можно указать атрибут openType с нужным
значением. Это избавляет от необходимости получать
ссылку на действие в контроллере и программно
устанавливать данное свойство. Например:
setWindowId() - позволяет задать идентификатор
экрана выбора сущности. По умолчанию используется
экран {имя_сущности}.lookup, например
sales$Customer.lookup. Если такого экрана не
существует, то делается попытка открыть экран
{имя_сущности}.browse, например
sales$Customer.browse.
customersTableRefresh.setRefreshParamsSupplier(() ->
ParamsMap.of("number",
/* some value dependent
on the current state of the screen */
));
<table id="usersTable">
<actions>
<action id="add" openType="DIALOG"/>
setWindowParams() - позволяет задать параметры
экрана выбора, передаваемые в его метод init(). Далее
эти параметры можно использовать напрямую в
запросе источника данных через префикс param$ или
инжектировать в контроллер экрана с помощью
аннотации @WindowParam.
setWindowParamsSupplier() отличается от
setWindowParams() тем, что позволяет получить
значения параметров непосредственно перед вызовом
действия. Полученные параметры объединяются с
предоставленными через setWindowParams() и могут
переопределять их. Например:
setHandler() - позволяет задать объект, реализующий
интерфейс Window.Lookup.Handler, передаваемый в
экран выбора. По умолчанию используется объект
класса AddAction.DefaultHandler.
ExcludeActionExcludeAction
ExcludeAction - действие с идентификатором excludeexclude.
Позволяет исключать экземпляры сущности из коллекции,
не удаляя их из базы данных. Класс данного действия
является наследником RemoveAction, однако при
срабатывании вызывает у CollectionDatasource не
removeItem(), а excludeItem(). Кроме того, для
вложенных источников данных ExcludeAction разрывает
связь с родительской сущностью, поэтому с помощью
данного действия можно организовать редактирование
ассоциации one-to-many.
В классе ExcludeAction в дополнение к RemoveAction
определены следующие специфические методы:
tableAdd.setWindowParamsSupplier(() ->
ParamsMap.of("customer", getItem()));
определены следующие специфические методы:
setConfirm() - показывать ли диалог подтверждения
удаления. Это свойство можно также установить через
конструктор действия. По умолчанию установлено в
false.
ExcelActionExcelAction
ExcelAction - действие с идентификатором excelexcel.
Предназначено для экспорта данных таблицы в формат
XLS и выгрузки соответствующего файла. Данное
действие можно связать только с компонентами Table,
GroupTable и TreeTable.
При программном создании действия можно задать
параметр конструктора display, передав реализацию
интерфейса ExportDisplay для выгрузки файла. По
умолчанию используется стандартная реализация.
В классе ExcelAction определены следующие
специфические методы:
setFileName() - позволяет задать имя выгружаемого
файла Excel без расширения.
getFileName() - возвращает имя выгружаемого файла
Excel без расширения.
5.5.4.2.2. 5.5.4.2.2. Стандартные действия поля выбора
Для компонентов PickerField, LookupPickerField и
SearchPickerField набор стандартных действий определен в
перечислении PickerField.ActionType. Реализации
являются внутренними классами интерфейса
PickerField.
Пример использования стандартных действий в
компоненте выбора:
<searchPickerField optionsDatasource="coloursDs"
LookupActionLookupAction
LookupAction - действие с идентификатором lookuplookup.
Предназначено для выбора экземпляра сущности и
установки его в качестве значения компонента. При
срабатывании открывает экран выбора сущностей.
В классе LookupAction определены следующие
специфические методы:
setLookupScreenOpenType() - позволяет задать режим
открытия экрана выбора сущности. По умолчанию экран
открывается в режиме THIS_TAB.
setLookupScreen() - позволяет задать идентификатор
экрана выбора сущности. По умолчанию используется
экран {имя_сущности}.lookup, например
sales$Customer.lookup. Если такого экрана не
существует, то делается попытка открыть экран
{имя_сущности}.browse, например
sales$Customer.browse.
setLookupScreenParams() - позволяет задать
параметры экрана выбора, передаваемые в его метод
init().
afterSelect() - вызывается действием после того, как
выбранный экземпляр установлен в качестве значения
компонента. Данный метод не имеет реализации и
может быть переопределен в наследниках для реакции
datasource="carDs"
property="colour">
<actions>
<action id="clear"/>
<action id="lookup"/>
<action id="open"/>
</actions>
</searchPickerField>
может быть переопределен в наследниках для реакции
на это событие.
afterCloseLookup() - вызывается действием в
последнюю очередь после закрытия экрана выбора,
независимо от того, был сделан выбор или нет. Данный
метод не имеет реализации и может быть
переопределен в наследниках для реакции на это
событие.
ClearActionClearAction
ClearAction - действие с идентификатором clearclear.
Предназначено для очистки (то есть установки в null)
текущего значения компонента.
OpenActionOpenAction
OpenAction - действие с идентификатором openopen.
Предназначено для открытия экрана редактирования
экземпляра сущности, являющегося текущим значением
компонента.
В классе OpenAction определены следующие
специфические методы:
setEditScreenOpenType() - позволяет задать режим
открытия экрана редактирования сущности. По
умолчанию экран открывается в режиме THIS_TAB.
setEditScreen() - позволяет задать идентификатор
экрана редактирования сущности. По умолчанию
используется экран {имя_сущности}.edit, например
sales$Customer.edit.
setEditScreenParams() - позволяет задать параметры
экрана редактирования, передаваемые в его метод
init().
afterWindowClosed() - вызывается действием после
закрытия экрана редактирования. Данный метод не
имеет реализации и может быть переопределен в
наследниках для реакции на это событие.
наследниках для реакции на это событие.
5.5.4.3. BaseAction5.5.4.3. BaseAction
BaseAction - базовый класс реализации действий. От него
рекомендуется наследовать собственные нестандартные
действия, если возможностей декларативного создания
действий не хватает.
При создании конкретного класса действия необходимо
определить метод actionPerform() и передать в
конструктор BaseAction идентификатор действия. Можно
также переопределить любые методы получения свойств
действия: getCaption(), getDescription(), getIcon(),
getShortcut(), isEnabled(), isVisible(). Стандартные
реализации этих методов возвращают значения,
установленные соответствующими set-методами.
Исключение составляет метод getCaption(): если
название действия явно не установлено методом
setCaption(), то он обращается в пакет локализованных
сообщений с именем, соответствующим пакету класса
действия, и возвращает сообщение с ключом, равным
идентификатору действия. Если сообщения с таким
ключом нет, то возвращается сам ключ, то есть
идентификатор действия.
В качестве альтернативы переопределению методов
можно использовать uent interface для установки свойств
и lambda expression для предоставления кода обработки
действия: см. методы withXYZ().
BaseAction может изменять свои свойства enabled и
visible в соответствии с разрешениями пользователя и
текущим контекстом.
BaseAction видим (visible), если:
метод setVisible(false) не вызывался;
метод setVisible(false) не вызывался;
для действия не установлено UI разрешение hide.
Действие разрешено (enabled), если:
метод setEnabled(false) не вызывался;
для действия не установлено UI разрешений hide или
read-only;
метод isPermitted() возвращает true;
метод isApplicable() возвращает true.
Примеры использования:
Действие кнопки:
В данном случае кнопка helloBtn получит в качестве
заголовка строку, находящуюся в пакете сообщений с
ключом hello. Для того, чтобы получить название
@Inject
private Button helloBtn;
@Override
public void init(Map<String, Object>params) {
helloBtn.setAction(new BaseAction("hello") {
@Override
public void actionPerform(Component
component) {
showNotification("Hello!",
NotificationType.TRAY);
}
});
// OR
helloBtn.setAction(new BaseAction("hello")
.withHandler(e ->
showNotification("Hello", NotificationType.TRAY)));
}
кнопки каким-либо иным способом, можно
переопределить метод getCaption() действия.
Действие кнопки программно создаваемого PickerField:
@Inject
private ComponentsFactory componentsFactory;
@Inject
private BoxLayout box;
@Override
public void init(Map<String, Object>params) {
PickerField pickerField =
componentsFactory.createComponent(PickerField.NAME);
pickerField.addAction(new BaseAction("hello") {
@Override
public String getCaption() {
return null;
}
@Override
public String getDescription() {
return getMessage("helloDescription");
}
@Override
public String getIcon() {
return"icons/hello.png";
}
@Override
public void actionPerform(Component
component) {
showNotification("Hello!",
NotificationType.TRAY);
Здесь анонимный класс-наследник BaseAction
используется для задания действия кнопки поля
выбора. Заголовок кнопки не отображается, вместо него
используется значок и описание, всплывающее при
наведении курсора мыши.
Действие таблицы:
NotificationType.TRAY);
}
});
// OR
pickerField.addAction(new BaseAction("hello")
.withCaption(null)
.withDescription(getMessage("helloDescription"))
.withIcon("icons/ok.png")
.withHandler(e ->
showNotification("Hello", NotificationType.TRAY)));
box.add(pickerField);
}
@Inject
private Table table;
@Inject
private Security security;
@Override
public void init(Map<String, Object> params) {
table.addAction(new HelloAction());
}
private class HelloAction extends BaseAction {
public HelloAction() {
Здесь объявлен класс HelloAction, экземпляр которого
добавляется в список действий таблицы. Действие
разрешено пользователям, имеющим специфическое
разрешение myapp.allow-greeting, и только когда
выбрана одна строка таблицы. Последнее условие
реализуется с помощью свойства target действия,
которое автоматически устанавливается когда
действие добавляется в ListComponent (Table или Tree).
Если необходимо действие, которое доступно, когда
выделены одна или более строк таблицы, удобно
воспользоваться наследником BaseAction - классом
ItemTrackingAction, который добавляет стандартную
public HelloAction() {
super("hello");
}
@Override
public void actionPerform(Component component) {
showNotification("Hello " +
table.getSingleSelected(), NotificationType.TRAY);
}
@Override
protected boolean isPermitted() {
return
security.isSpecificPermitted("myapp.allow-
greeting");
}
@Override
public boolean isApplicable() {
return target != null &&
target.getSelected().size() == 1;
}
}
ItemTrackingAction, который добавляет стандартную
реализацию метода isApplicable():
5.5.5. 5.5.5. Диалоговые окна и уведомления
Для вывода сообщений пользователю можно
использовать диалоговые окна и уведомления.
Диалоговые окна имеют заголовок с кнопкой закрытия и
отображаются всегда в центре главного окна
приложения. Уведомления могут отображаться как в
центре, так и в углу приложения, и автоматически
исчезать.
5.5.5.1. 5.5.5.1. Диалоговые окна
Диалоги общего назначения
Диалоговые окна общего назначения вызываются
методами showMessageDialog() и showOptionDialog()
@Inject
private Table table;
@Override
public void init(Map<String, Object> params) {
table.addAction(new ItemTrackingAction("hello")
{
@Override
public void actionPerform(Component
component) {
showNotification("Hello " +
table.getSelected().iterator().next(),
NotificationType.TRAY);
}
});
}
методами showMessageDialog() и showOptionDialog()
интерфейса Frame. Этот интерфейс реализуется
контроллером экрана, поэтому данные методы можно
вызывать напрямую в коде контроллера.
showMessageDialog() предназначен для отображения
сообщения. Метод принимает следующие параметры:
title - заголовок диалогового окна.
message - сообщение. В случае HTML-типа (см. ниже) в
сообщении можно использовать теги HTML для
форматирования. При использовании HTML
обязательно экранируйте данных из БД во избежание
code injection в веб-клиенте. В не-HTML сообщениях
можно использовать символы \n для переноса строки.
messageType - тип сообщения. Возможные типы:
CONFIRMATION, CONFIRMATION_HTML - диалог
подтверждения.
WARNING, WARNING_HTML - диалог преупреждения.
Различие типов сообщений отражается только в
пользовательском интерфейсе десктоп-приложений.
Типы сообщений могут быть установлены с
параметрами:
width - ширина диалога,
modal - модальность диалога,
maximized - должен ли диалог быть развёрнут во весь
экран,
closeOnClickOutside - возможность закрыть диалог
кликом по любой области за его пределами.
Пример вызова диалога:
showMessageDialog("Warning", "Something is
wrong",
MessageType.WARNING.modal(true).closeOnClickOutsi
de(true));
showOptionDialog() предназначен для отображения
сообщения и кнопок для выбора пользователем.
Метод в дополнение к параметрам, описанным для
showMessageDialog(), принимает массив или список
действий. Для каждого действия в диалоге создается
кнопка, при нажатии на которую пользователем
диалог закрывается и вызывается метод
actionPerform() данного действия.
В качестве кнопок со стандартными названиями и
значками удобно использовать анонимные классы,
унаследованные от DialogAction. Поддерживаются
пять видов действий, определяемых перечислением
DialogAction.Type: OK, CANCEL, YES, NO, CLOSE. Названия
соответствующих кнопок извлекаются из главного
пакета локализованных сообщений.
Пример вызова диалога с кнопками Да и Нет и с
заголовком и сообщением, взятыми из пакета
локализованных сообщений текущего экрана:
Параметр Status конструктора DialogAction
de(true));
showOptionDialog(
getMessage("confirmCopy.title"),
getMessage("confirmCopy.msg"),
MessageType.CONFIRMATION,
new Action[] {
new DialogAction(DialogAction.Type.YES,
Status.PRIMARY).withHandler(e -> copySettings()),
new DialogAction(DialogAction.Type.NO,
Status.NORMAL)
}
);
Параметр Status конструктора DialogAction
используется для определения визуального стиля
кнопки, к которой привязано данное действие. Статус
Status.PRIMARY подсвечивает кнопку и задаёт ей
выделение по умолчанию. Параметр Status можно не
использовать, в этом случае используется подсветка
кнопок по умолчанию. Если в showOptionDialog
передано несколько действий с Status.PRIMARY, то
фокус и стиль получает только кнопка первого такого
действия в списке.
Диалог загрузки файлов
Диалоговое окно FileUploadDialog предоставляет
базовую функциональность загрузки файлов в
промежуточное хранилище. Оно содержит drop zone для
перетаскивания файлов извне браузера и кнопку
загрузки файла.
Открыть диалог можно с помощью метода openWindow(),
в случае успешной загрузки окно будет закрыто с
COMMIT_ACTION_ID. Закрытие диалога можно
отслеживать с помощью слушателей CloseListener и
CloseWithCommitListener. Чтобы получить UUID и имя
загруженного файла, используйте методы getFileId() и
getFileName(). Затем для файла можно, например,
создать FileDescriptor, позволяющий ссылаться на
него из объектов модели данных, или реализовать
другую логику.
5.5.5.2. 5.5.5.2. Уведомления
Уведомления вызываются методом showNotification()
интерфейса Frame. Этот интерфейс реализуется
контроллером экрана, поэтому данный метод можно
вызывать напрямую в коде контроллера.
Метод showNotification() принимает следующие
параметры:
caption - текст уведомления. В случае HTML-типа (см.
ниже) в сообщении можно использовать теги HTML для
форматирования. При использовании HTML обязательно
экранируйте данных из БД во избежание code injection в
веб-клиенте. В не-HTML сообщениях можно
использовать символы \n для переноса строки.
description - опциональное описание, которое будет
отображено ниже caption. Также можно использовать
символы \n или HTML-форматирование.
type - тип уведомления. Возможные типы:
FileUploadDialog dialog = (FileUploadDialog)
openWindow("fileUploadDialog", OpenType.DIALOG);
dialog.addCloseWithCommitListener(() -> {
UUID fileId = dialog.getFileId();
String fileName = dialog.getFileName();
FileDescriptor fileDescriptor =
fileUploadingAPI.getFileDescriptor(fileId, fileName);
// your logic here
});
ДЕМОНСТРАЦИЯ
TRAY, TRAY_HTML - уведомление показывается в правом
нижнем углу приложения и исчезает автоматически.
HUMANIZED, HUMANIZED_HTML - стандартное уведомление
в центре экрана, исчезает автоматически.
WARNING, WARNING_HTML - предупреждение. Исчезает при
клике пользователя.
ERROR, ERROR_HTML - уведомление об ошибке. Исчезает
при клике пользователя.
Примеры вызова уведомлений:
5.5.6. 5.5.6. Фоновые задачи
Механизм фоновых задач
предназначен для асинхронного выполнения длительных
операций на клиентском уровне без заморозки
пользовательского интерфейса.
Использование фоновых задач:
1. Задача описывается как наследник абстрактного
класса BackgroundTask. В конструктор задачи
необходимо передать ссылку на контроллер экрана, с
которым будет связана задача, и значение таймаута ее
выполнения.
Если экран указан, то при его закрытии пользователем
активная задача будет прервана. Кроме того, задача
будет автоматически прервана по истечении
указанного таймаута.
showNotification(getMessage("selectBook.text"),
NotificationType.HUMANIZED);
showNotification("Validation error", "<b>Date</b> is
incorrect", NotificationType.TRAY_HTML);
Собственно действия, выполняемые задачей,
реализуются в методе run().
2. Создается объект управления задачей −
BackgroundTaskHandler. Для этого экземпляр задачи
необходимо передать методу handle() бина
BackgroundWorker. Ссылку на BackgroundWorker можно
получить инжекцией в контроллер экрана, либо
статическим методом класса AppBeans.
3. Выполняется запуск задачи.
Пример:
@Inject
protected BackgroundWorker backgroundWorker;
@Override
public void init(Map<String, Object> params) {
// Create task with 10 sec timeout and this screen
as owner
BackgroundTask<Integer, Void> task = new
BackgroundTask<Integer, Void>(10, this) {
@Override
public Void run(TaskLifeCycle<Integer>
taskLifeCycle) throws Exception {
// Do something in background thread
for (int i = 0; i < 5; i++) {
TimeUnit.SECONDS.sleep(1);
// time
consuming computations
taskLifeCycle.publish(i);
// publish
current progress to show it in progress() method
}
return null;
}
@Override
public void canceled() {
Подробная информация о назначении методов приведена
в JavaDocs классов BackgroundTask, TaskLifeCycle,
BackgroundTaskHandler.
Ниже приведены моменты, на которые следует обратить
внимание:
BackgroundTask<T, V> − параметризованный класс:
T − тип объектов, показывающих прогресс задачи.
Объекты этого типа передаются в метод progress()
задачи при вызове TaskLifeCycle.publish() в рабочем
потоке.
V − тип результата задачи, он передается в метод
done(). Его также можно получить вызовом метода
BackgroundTaskHandler.getResult(), что приведет к
public void canceled() {
// Do something in UI thread if the task
is canceled
}
@Override
public void done(Void result) {
// Do something in UI thread when the task
is done
}
@Override
public void progress(List<Integer> changes) {
// Show current progress in UI thread
}
};
// Get task handler object and run the task
BackgroundTaskHandler taskHandler =
backgroundWorker.handle(task);
taskHandler.execute();
}
BackgroundTaskHandler.getResult(), что приведет к
ожиданию завершения задачи.
Метод canceled() вызывается только в случае
управляемой отмены задачи, то есть при вызове
cancel() у TaskHandler.
Метод handleTimeoutException() вызывается при
истечении таймаута задачи. Если окно, в котором
выполняется задача, закрывается, то задача
останавливается без оповещения.
Метод run() задачи должен поддерживать
возможность прерывания извне. Для этого в долгих
процессах желательно периодически проверять флаг
TaskLifeCycle.isInterrupted(), и соответственно
завершать выполнение. Кроме того, нельзя тихо
проглатывать исключение InterruptedException (или
вообще все исключения). Вместо этого нужно либо
вообще не перехватывать его, либо выполнять
корректный выход из метода.
Объекты BackgroundTask не имеют состояния. Если при
реализации конкретного класса задачи не заводить
полей для хранения промежуточных данных, то можно
запускать несколько параллельно работающих
процессов, используя единственный экземпляр задачи.
Объект BackgroundHandler можно запускать (т.е.
вызывать его метод execute()) всего один раз. Если
требуется частый перезапуск задачи, то используйте
класс BackgroundTaskWrapper.
Для показа пользователю модального окна с
прогрессом и кнопкой Отмена используйте классы
BackgroundWorkWindow или
BackgroundWorkProgressWindow с набором статических
методов.Для окна можно задать режим отображения
прогресса и разрешить или запретить отмену фоновой
задачи.
задачи.
Если внутри потока задачи необходимо использовать
некоторые значения визуальных компонентов, то нужно
реализовать их получение в методе getParams(),
который выполняется в потоке UI один раз при запуске
задачи. В методе run() эти параметры будут доступны
через метод getParams() объекта TaskLifeCycle.
При возникновении исключительных ситуаций в потоке
UI вызывается метод
BackgroundTask.handleException(), в котором можно
отобразить ошибку.
На выполнение фоновых задач влияют свойства
приложения cuba.backgroundWorker.maxActiveTasksCount и
cuba.backgroundWorker.timeoutCheckInterval.
В блоке Web Client фоновые задачи используют
технологию HTTP push, предоставляемую
фреймворком Vaadin. См. https://vaadin.com/wiki/-
/wiki/Main/Working+around+push+issues для получения
информации о настройке веб-серверов для
использования данной технологии.
Если вы не используете фоновую задачу, но хотите
изменять состояние UI-компонентов из не-UI потока,
воспользуйтесь методами интерфейса UIAccessor.
Получите ссылку на интерфейс UIAccessor методом
BackgroundWorker.getUIAccessor() в UI-потоке, и
после этого вы сможете вызывать его методы
access() и accessSynchronously() из фонового
потока для безопасного чтения и изменения
состояния UI-компонентов.
5.5.7. 5.5.7. Темы приложения
Тема служит для управления визуальным
Тема служит для управления визуальным
представлением приложения.
5.5.7.1. 5.5.7.1. Тема в веб--приложениях
Тема веб-приложения состоит из файлов SCSS и других
ресурсов, в том числе файлов изображений.
5.5.7.1.1. 5.5.7.1.1. Использование существующих тем
Платформа включает в себя две готовые темы: Halo и
Havana. Приложение будет по умолчанию использовать ту
из них, которая указана в свойстве приложения
cuba.web.theme. Пользователь может выбрать другую
доступную тему в стандартном экране HelpHelp → SettingsSettings. Если
вы не хотите, чтобы пользователи имели возможность
сами выбирать тему, зарегистрируйте экран settings в
файле web-screens.xml проекта с параметром
changeThemeEnabled = false:
5.5.7.1.2. 5.5.7.1.2. Расширение существующей темы
Существующая в платформе тема может быть изменена в
проекте приложения. В измененной теме можно сделать
следующее:
Изменить изображения для фирменного стиля.
Добавить изображения для использования в
визуальных компонентах.
Создать новые стили и использовать их в атрибутах
<screen id="settings"
template="/com/haulmont/cuba/web/app/ui/core/settings/
settings-window.xml">
<param name="changeThemeEnabled" value="false"/>
</screen>
Создать новые стили и использовать их в атрибутах
stylename визуальных компонентов. Для этого требуется
знание CSS.
Изменить существующие в платформе стили
компонентов.
Изменить общие параметры, такие как цвет фона,
отступы, промежутки и т.д.
Тема описывается в файлах SCSS. Для изменения
(расширения) темы в проекте необходимо создать
специальную файловую структуру в модуле webweb. Это
удобно сделать с помощью CUBA StudioCUBA Studio: откройте секцию
Project propertiesProject properties и нажмите ссылку Create theme extensionCreate theme extension. В
диалоговом окне выберите тему, которую вы хотите
расширить. В результате в проекте будет создана
следующая структура каталогов (для расширения темы
Halo):
Кроме того, скрипт сборки build.gradle будет дополнен
задачей buildScssThemes, автоматически запускаемой
при сборке модуля webweb. Опциональная задача deployThemes
может быть использована для быстрого применения
изменений в темах на работающем приложении.
themes/
halo/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
halo-ext.scss
halo-ext-defaults.scss
favicon.ico
styles.scss
Если ваше приложение включает в себя компонент с
расширением темы и вы хотите применить это
расширение ко всему приложению, в этом случае
необходимо создать расширение темы и для базового
проекта. Подробнее о наследовании тем смотрите в
разделе Наследование тем из компонентов
приложения.
Изменение фирменного стиля
Можно настроить некоторые параметры фирменного
стиля (branding): пиктограммы и заголовки окна логина и
главного окна, пиктограмму вебсайта favicon.ico.
Для использования собственных изображений, замените
соответствующие файлы в каталоге
modules/web/themes/halo/branding.
Чтобы задать заголовки главного окна, окна логина и
текст приглашения окна логина, в CUBA Studio откройте
Project propertiesProject properties → EditEdit и нажмите кнопку BrandingBranding внизу
страницы. Используйте соответствующие ссылки для
задания заголовков окон и текста приглашения окна
логина.
Данные параметры сохраняются в главном пакете
сообщений модуля webweb (то есть в файле
modules/web/<root_package>/web/messages.properties
и его вариантах для разных локалей). Использование
пакетов сообщений дает возможность использовать
разные файлы изображений для разных локалей
пользователей. Пример содержимого файла
messages.properties:
application.caption = MyApp
application.logoImage = branding/myapp-menu.png
Путь к favicon.ico указывать не нужно, он должен
обязательно находится в корне каталога с именем темы.
Добавление шрифтов
В приложение можно добавить собственные шрифты.
Для добавления семейства шрифтов импортируйте его в
первой строке файла styles.scss, например:
Добавление изображений
В расширенную тему можно также добавить файлы
изображений для использования в свойствах icon
действий и визуальных компонентов, например Button.
Например, чтобы добавить расширение темы Halo
пиктограмму, достаточно в описанный выше каталог
modules/web/themes/halo добавить файл изображения
(желательно в некоторый подкаталог):
После этого можно использовать эту пиктограмму,
указывая в свойстве icon путь к ней относительно
каталога темы:
loginWindow.caption = MyApp Login
loginWindow.welcomeLabel = Welcome to MyApp!
loginWindow.logoImage = branding/myapp-login.png
@import url(http://fonts.googleapis.com/css?
family=Roboto);
themes/
halo/
images/
address-book.png
Вместо файлов изображений для пиктограмм можно
использовать элементы шрифта Font Awesome. Для этого
достаточно указать в свойстве icon имя нужной
константы перечисления
com.vaadin.server.FontAwesome с префиксом font-
icon:, например:
Добавление изображений из других библиотек шрифтов
Для более тонкой настройки расширенной темы можно
создать изображения, встроенные в шрифты, либо
использовать готовые внешние библиотеки
изображений.
Создайте в модуле webweb класс enum, реализующий
интерфейс com.vaadin.server.FontIcon, в который
поместите новые изображения:
<action id="adresses"
icon="images/address-book.png"/>
<action id="adresses"
icon="font-icon:BOOK"/>
import com.vaadin.server.FontIcon;
import com.vaadin.server.GenericFontIcon;
public enum IcoMoon implements FontIcon {
HEADPHONES(0XE900),
SPINNER(0XE905);
public static final String FONT_FAMILY =
"IcoMoon";
private int codepoint;
IcoMoon(int codepoint) {
this.codepoint = codepoint;
}
@Override
public String getFontFamily() {
return FONT_FAMILY;
}
@Override
public int getCodepoint() {
return codepoint;
}
@Override
public String getHtml() {
return GenericFontIcon.getHtml(FONT_FAMILY,
codepoint);
}
@Override
public String getMIMEType() {
throw new
UnsupportedOperationException(FontIcon.class.getSimpl
eName()
+ " should not be used where a MIME
type is needed.");
}
public static IcoMoon fromCodepoint(final int
codepoint) {
for (IcoMoon f : values()) {
if (f.getCodepoint() == codepoint) {
return f;
}
}
Добавьте новые стили и файлы шрифта в расширение
темы. Рекомендуется создать отдельную папку fonts в
главном каталоге расширения темы, например,
modules/web/themes/halo/com.company.demo/fonts.
Поместите в неё стили и файлы шрифтов в своих
собственных подпапках, например, fonts/icomoon.
Файлы шрифта включают в себя набор следующих
расширений: .eot, .svg, .ttf, .woff. Использованный в
этом примере набор шрифтов icomoon из открытой
библиотеки представлен в виде 4 файлов: icomoon.eot,
icomoon.svg, icomoon.ttf, icomoon.woff, которые
используются совместно.
Создайте файл стилей, в который включите @font-face и
CSS класс со стилем для изображения. Ниже
представлен пример файла icomoon.scss, где имя
класса IcoMoon соответствует значению, возвращаемому
методом FontIcon#getFontFamily:
}
throw new
IllegalArgumentException("Codepoint " + codepoint
+ " not found in IcoMoon");
}
}
@mixin icomoon-style {
/* use !important to prevent issues with browser
extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
Подключите файл стилей шрифта в halo-ext.scss или
другой файл расширения данной темы:
Создайте новый класс App.java, расширяющий
DefaultApp, в корневом пакете модуля webweb и
зарегистрируйте его в web-spring.xml в качестве бина
cuba_App, чтобы переопределить бин центрального
класса инфраструктуры приложения, например:
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@font-face {
font-family: 'icomoon';
src:url('icomoon.eot?hwgbks');
src:url('icomoon.eot?hwgbks#iefix')
format('embedded-opentype'),
url('icomoon.ttf?hwgbks')
format('truetype'),
url('icomoon.woff?hwgbks') format('woff'),
url('icomoon.svg?hwgbks#icomoon')
format('svg');
font-weight: normal;
font-style: normal;
}
.IcoMoon {
@Include icomoon-style;
}
@import "fonts/icomoon/icomoon";
Зарегистрируйте в App.java новый алиас для
изображений:
Теперь вы можете использовать изображения по прямой
ссылке на класс и элемент enum в XML-дескрипторе
экрана:
или в контроллере Java:
В результате, новые изображения добавились к кнопкам:
<bean name="cuba_App"
class="com.company.sample.web.App"
scope="prototype"/>
import com.haulmont.cuba.web.DefaultApp;
import
com.haulmont.cuba.web.gui.components.WebComponentsHel
per;
public class App extends DefaultApp {
static {
WebComponentsHelper.registerFontIcon("ico-
moon-icon", IcoMoon.class);
}
}
<button caption="Headphones" icon="ico-moon-
icon:HEADPHONES"/>
spinnerBtn.setIcon("ico-moon-icon:SPINNER");
Создание новых стилей
Рассмотрим пример установки желтого цвета фона для
поля, отображающего название заказчика.
В XML-дескрипторе экрана определен компонент
FieldGroup:
Элементы field компонента FieldGroup не имеют
атрибута stylename, поэтому необходимо задать имя
стиля в контроллере:
В файле halo-ext.scss добавьте определение нового
стиля в mixin halo-ext:
<fieldGroup id="fieldGroup" datasource="customerDs">
<field property="name"/>
<field property="address"/>
</fieldGroup>
@Named("fieldGroup.name")
private TextField nameField;
@Override
public void init(Map<String, Object> params) {
nameField.setStyleName("name-field");
}
@import "../halo";
@mixin halo-ext {
@include halo;
После пересборки проекта поля будут выглядеть
следующим образом:
Изменение существующих стилей компонентов
Для изменения параметров стиля существующих
компонентов необходимо добавить соответствующий
код CSS в mixin halo-ext файла halo-ext.scss. Например,
для того, чтобы пункты главного меню отображались
жирным шрифтом, содержимое файла halo-ext.scss
должно быть следующим:
Изменение общих параметров
Темы содержат переменные SCSS, которые управляют
цветом фона, размерами компонентов, отступами и пр.
Рассмотрим пример расширения темы Halo, так как она
@include halo;
.name-field {
background-color: lightyellow;
}
}
@import "../halo/halo";
@mixin halo-ext {
@include halo;
.v-menubar-menuitem-caption {
font-weight: bold;
}
}
Рассмотрим пример расширения темы Halo, так как она
основана на теме Valo фреймворка VaadinVaadin, и
предоставляет максимальные возможности адаптации.
Файл themes/halo/halo-ext-defaults.scss
предназначен для размещения в нем переменных темы.
Большинство переменных Halo соответствует описанным
в документации по Valo, ниже приведены основные:
$v-background-color: #fafafa;
/* component
background colour */
$v-app-background-color: #e7ebf2;
/* application
background colour */
$v-panel-background-color: #fff;
/* panel
background colour */
$v-focus-color: #3b5998;
/* focused
element colour */
$v-error-indicator-color: #ed473b;
/* empty
required fields colour */
$v-line-height: 1.35;
/* line height
*/
$v-font-size: 14px;
/* font size */
$v-font-weight: 400;
/* font weight
*/
$v-unit-size: 30px;
/* base theme
size, defines the height for buttons, fields and
other elements */
$v-font-size--h1: 24px;
/* h1-style
Label size */
$v-font-size--h2: 20px;
/* h2-style
Label size */
$v-font-size--h3: 16px;
/* h3-style
Label size */
/* margins for containers */
Пример содержимого файла halo-ext-defaults.scss
для темы с темным фоном и немного уменьшенными
$v-layout-margin-top: 10px;
$v-layout-margin-left: 10px;
$v-layout-margin-right: 10px;
$v-layout-margin-bottom: 10px;
/* spacing between components in a container (if
enabled) */
$v-layout-spacing-vertical: 10px;
$v-layout-spacing-horizontal: 10px;
/* basic table dimensions */
$v-table-row-height: 30px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 7px;
/* input field focus style */
$v-focus-style: inset 0px 0px 5px 1px rgba($v-focus-
color, 0.5);
/* required fields focus style */
$v-error-focus-style: inset 0px 0px 5px 1px rgba($v-
error-indicator-color, 0.5);
/* animation for elements is enabled by default */
$v-animations-enabled: true;
/* popup window animation is disabled by default */
$v-window-animations-enabled: false;
/* inverse header is controlled by
cuba.web.useInverseHeader property */
$v-support-inverse-menu: true;
/* show "required" indicators for components */
$v-show-required-indicators: false !default;
отступами:
Переход с темы Havana Havana на полнофункциональную Halo Halo
Тема Halo лучше поддаётся расширению, в ней
поддерживаются новые визуальные компоненты, такие
как DataGrid и SideMenu. Если вы хотите использовать эти
компоненты, а также получать обновления библиотеки
визуальных компонентов, рекомендуется использовать
тему Halo. В то же время, если вам важно сохранить
внешний вид темы Havana, вы можете использовать
следующие переменные в файле halo-ext-
defaults.scss:
$v-background-color: #444D50;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
$v-support-inverse-menu: false;
$cuba-menubar-background-color: #315379;
$cuba-menubar-border-color: #315379;
Изменение заголовка приложения
Тема Halo поддерживает свойство приложения
cuba.web.useInverseHeader, управляющее цветом заголовка
приложения. По умолчанию это свойство установлено в
true, что задает темный (инверсный) заголовок. В
проекте можно не изменяя темы сделать заголовок
светлым, установив данное свойство в false.
$cuba-menubar-border-color: #315379;
$v-table-row-height: 25px;
$v-selection-color: rgb(77, 122, 178);
$v-table-header-font-size: 12px;
$v-textfield-border: 1px solid #A5C4E0;
$v-selection-item-selection-color: #4D7AB2;
$v-app-background-color: #E3EAF1;
$v-font-size: 12px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-border-radius: 0px;
$v-border: 1px solid #9BB3D3 !default;
$v-font-family:
Verdana,tahoma,arial,geneva,helvetica,sans-
serif,"Trebuchet MS";
$v-panel-background-color: #ffffff;
$v-background-color: #ffffff;
$cuba-menubar-menuitem-text-color: #ffffff;
$cuba-app-menubar-padding-top: 8px;
$cuba-app-menubar-padding-bottom: 8px;
$cuba-menubar-text-color: #ffffff;
$cuba-menubar-submenu-padding: 1px;
5.5.7.1.3. 5.5.7.1.3. Создание новой темы
В проекте можно создать одну или несколько новых тем и
дать возможность пользователям выбирать среди них
подходящую. Создание новой темы позволяет также
переопределять переменные файла *-theme.properties,
задающие некоторые параметры, требуемые на стороне
сервера:
Размеры диалоговых окон по умолчанию.
Ширина полей ввода по умолчанию.
Размеры некоторых компонентов (Filter,
FileMultiUploadField).
Соответствие между именами пиктограмм и именами
констант перечисления
com.vaadin.server.FontAwesome для использования
элементов шрифта Font AwesomeFont Awesome в стандартных
действиях и экранах платформы при включенном
свойстве cuba.web.useFontIcons.
Создание новой темы
Рассмотрим пример создания на основе Halo новой темы
Facebook, напоминающей интерфейс сайта известной
социальной сети.
1. В CUBA StudioCUBA Studio откройте секцию Project PropertiesProject Properties и
нажмите ссылку Create theme extensionCreate theme extension. В диалоговом
окне выберите halo и нажмите CreateCreate. В проекте будет
создано расширение темы Halo, как описано в
предыдущем разделе.
2. Переименуйте созданный в модуле webweb каталог
themes/halo в themes/facebook, аналогично замените
префикс halo на facebook в именах файлов.
3. Скопируйте в themes/facebook файл halo-ext.scss и
переименуйте его в facebook.scss, также скопируйте
и переименуйте halo-ext-defaults.scss в facebook-
и переименуйте halo-ext-defaults.scss в facebook-
defaults.scss.
4. Создайте новый каталог com.haulmont.cuba в
themes/facebook и скопируйте в него файл app-
component.scss. В итоге должна получиться
следующая структура:
5. Отредактируйте файл styles.scss:
themes/
facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
facebook-ext.scss //
theme SCSS
facebook-ext-defaults.scss //
theme customizations
com.haulmont.cuba/
app-component.scss //
cuba app-component include
facebook.scss //
main theme file
facebook-defaults.scss //
main theme variables
favicon.ico
styles.scss //
entry point of SCSS build procedure
@import "facebook-defaults";
@import "facebook";
.facebook {
@include facebook;
6. Отредактируйте файл facebook.scss:
7. Отредактируйте файл app-component.scss из
каталога com.haulmont.cuba:
8. Скопируйте в файл facebook-defaults.scss
следующие переменные:
@include facebook;
}
@import "../halo/halo";
@mixin facebook {
@include halo;
}
@import "../facebook";
@mixin com_haulmont_cuba {
@include facebook;
}
@import "../halo/halo-defaults";
$v-background-color: #fafafa;
$v-app-background-color: #e7ebf2;
$v-panel-background-color: #fff;
$v-focus-color: #3b5998;
$v-border-radius: 0;
$v-textfield-border-radius: 0;
$v-font-family: Helvetica, Arial, 'lucida grande',
$v-font-family: Helvetica, Arial, 'lucida grande',
tahoma, verdana, arial, sans-serif;
$v-font-size: 14px;
$v-font-color: #37404E;
$v-font-weight: 400;
$v-link-text-decoration: none;
$v-shadow: 0 1px 0 (v-shade 0.2);
$v-bevel: inset 0 1px 0 v-tint;
$v-unit-size: 30px;
$v-gradient: v-linear 12%;
$v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px
(v-shade 0.7);
$v-shadow-opacity: 20%;
$v-selection-overlay-padding-horizontal: 0;
$v-selection-overlay-padding-vertical: 6px;
$v-selection-item-border-radius: 0;
$v-line-height: 1.35;
$v-font-size: 14px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
9. Файлы в каталоге com.company.application
используются, чтобы модифицировать тему для
конкретного проекта. В данном примере мы не будет
добавлять к теме facebook дополнительных
модификаций, поэтому удалите содержимое этих
файлов и оставьте их пустыми.
10. Создайте в подкаталоге src модуля webweb файл
facebook-theme.properties:
и поместите в него следующую строку:
При необходимости в этом файле можно
переопределять server-side переменные темы,
заданные в файле halo-theme.properties
платформы.
11. В файл web-app.properties добавьте следующие
свойства:
$v-table-cell-padding-horizontal: 5px;
$v-focus-style: inset 0px 0px 1px 1px rgba($v-
focus-color, 0.5);
$v-error-focus-style: inset 0px 0px 1px 1px
rgba($v-error-indicator-color, 0.5);
modules/
web/
src/
com.company.application/
web/
facebook-theme.properties
@include=halo-theme.properties
Свойство приложения cuba.themeCong определяет,
какие темы будут доступны в меню приложения
SettingsSettings.
12. Пересоберите приложение и запустите сервер. Теперь
при первом входе пользователь увидит приложение в
теме Facebook, и в окне HelpHelp → SettingsSettings сможет выбирать
между темами FacebookFacebook, HaloHalo и HavanaHavana.
Изменение server-side server-side параметров темы
В теме Halo по умолчанию (при включенном свойстве
приложения cuba.web.useFontIcons) пиктограммы
стандартных действий и экранов платформы
загружаются из шрифта Font AwesomeFont Awesome. В этом случае
можно заменить стандартную пиктограмму, задав в
файле <your_theme>-theme.properties нужное
соответствие между именем пиктограммы и именем
элемента шрифта. Например, чтобы использовать
значок "плюс" для действия create в новой теме Facebook,
содержимое файла web/src/facebook-
theme.properties должно быть следующим:
Фрагмент стандартного экрана списка пользователей в
теме Facebook и с измененным значком действия create:
cuba.web.theme = facebook
cuba.themeConfig = havana-theme.properties halo-
theme.properties facebook-theme.properties
@include=halo-theme.properties
cuba.web.icons.create.png = PLUS
5.5.7.1.4. 5.5.7.1.4. Наследование тем из компонентов приложения
Если ваш проект включает в себя компонент с новой
темой, вы можете настроить использование этой темы во
всём проекте.
1. В этом примере мы вновь используем тему facebook из
предыдущего примера. Создайте тему facebook для
компонента приложения, следуя инструкции из
раздела Создание новой темы.
2. Установите компонент, используя меню Studio, как
описано в разделе Использование компонентов
приложения.
3. Расширьте тему halo в проекте, в котором используется
ваш компонент.
4. В IDE переименуйте все вхождения halo в каталоге
themes, включая имена файлов, в facebook. В итоге у вас
должна получиться следующая структура:
themes/
facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
facebook-ext.scss
facebook-ext-defaults.scss
5. Файл app-component.scss группирует модификации
темы в конкретном компоненте приложения. В
процессе сборки SCSS плагин Gradle автоматически
находит компоненты и включает их в генерируемый
файл modules/web/build/themes-
tmp/VAADIN/themes/{theme-name}/app-
components.scss.
По умолчанию переменные темы из {theme-name}-ext-
defaults не наследуются в проект. Чтобы изменить это
поведение, вручную добавьте включение в файл app-
component.scss:
На этом этапе тема facebook уже импортирована в
проект из компонента приложения.
6. Вы можете использовать файлы facebook-ext.scss и
facebook-ext-defaults.scss из пакета
com.company.application, чтобы переопределить
переменные темы компонента и модифицировать её
для конкретного проекта. В данном примере мы будем
использовать тему facebook как она есть, поэтому
оставим эти файлы пустыми.
7. Добавьте свойства приложения в файл web-
app.properties, чтобы сделать тему facebook
facebook-ext-defaults.scss
favicon.ico
styles.scss
@import "facebook-ext";
@import "facebook-ext-defaults";
@mixin com_company_application {
@include com_company_application-facebook-ext;
}
app.properties, чтобы сделать тему facebook
доступной в меню приложения SettingsSettings. Используйте
относительный путь для ссылки на файл facebook-
theme.properties.
Если при сборке тем возникли проблемы, проверьте
каталог modules/web/build/themes-tmp. В нём
находятся генерируемые файлы и включения app-
component.scss, которые можно использовать для
поиска проблем компиляции SCSS.
5.5.7.1.5. 5.5.7.1.5. Повторное использование тем
Любую тему можно создать и использовать отдельно от
компонента приложения. Для создания темы, которую
можно использовать повторно, необходимо создать с нуля
отдельный Java-проект и собрать его в единый JAR-файл.
Ниже приведена инструкция, как подготовить тему
facebook из предыдущих примеров для многократного
использования.
1. Создайте в IDE новый Java-проект, содержащий файлы
SCSS и свойства темы, со следующей структурой:
cuba.web.theme = facebook
cuba.themeConfig = havana-theme.properties halo-
theme.properties com/company/{app-component-
name}/facebook-theme.properties
halo-facebook/
src/
//sources root
halo-facebook/
com.haulmont.cuba/
app-component.scss
Этот проект также доступен на GitHub.
содержание скрипта build.gradle:
содержание файла settings.gradle:
содержание файла app-component.scss:
halo-facebook.scss
halo-facebook-defaults.scss
halo-facebook-theme.properties
styles.scss
allprojects {
group = 'com.haulmont.theme'
version = '0.1'
}
apply(plugin: 'java')
apply(plugin: 'maven')
sourceSets {
main {
java {
srcDir 'src'
}
resources {
srcDir 'src'
}
}
}
rootProject.name = 'halo-facebook'
@import "../halo-facebook";
содержание файла halo-facebook.scss:
содержание файла halo-facebook-defaults.scss:
@import "../halo-facebook";
@mixin com_haulmont_cuba {
@include halo-facebook;
}
@import "../halo/halo";
@mixin halo-facebook {
@include halo;
}
@import "../halo/halo-defaults";
$v-background-color: #fafafa;
$v-app-background-color: #e7ebf2;
$v-panel-background-color: #fff;
$v-focus-color: #3b5998;
$v-border-radius: 0;
$v-textfield-border-radius: 0;
$v-font-family: Helvetica, Arial, 'lucida grande',
tahoma, verdana, arial, sans-serif;
$v-font-size: 14px;
$v-font-color: #37404E;
$v-font-weight: 400;
$v-link-text-decoration: none;
$v-shadow: 0 1px 0 (v-shade 0.2);
$v-bevel: inset 0 1px 0 v-tint;
$v-unit-size: 30px;
$v-gradient: v-linear 12%;
$v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px
(v-shade 0.7);
содержание файла halo-facebook-theme.properties:
(v-shade 0.7);
$v-shadow-opacity: 20%;
$v-selection-overlay-padding-horizontal: 0;
$v-selection-overlay-padding-vertical: 6px;
$v-selection-item-border-radius: 0;
$v-line-height: 1.35;
$v-font-size: 14px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;
$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;
$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;
$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;
$v-focus-style: inset 0px 0px 1px 1px rgba($v-
focus-color, 0.5);
$v-error-focus-style: inset 0px 0px 1px 1px
rgba($v-error-indicator-color, 0.5);
$v-show-required-indicators: true;
2. Соберите и установите проект с помощью задачи Gradle:
3. Теперь добавьте эту тему в свой CUBA-проект в качестве
зависимости Maven в двух конфигурациях: themes и
compile, добавив в build.gradle следующие строки:
Если вы установили тему локально, не забудьте
добавить локальный репозиторий Maven к списку
используемых в проекте репозиториев. Для этого
перейдите на вкладку AdvancedAdvanced в окне Studio ProjectProject
PropertiesProperties и установите флажок Use local Maven repositoryUse local Maven repository.
4. Чтобы унаследовать тему и добавить модификации для
конкретного проекта, необходимо сначала расширить
эту тему. Расширьте тему halo и переименуйте каталог
themes/halo в themes/halo-facebook:
@include=halo-theme.properties
gradle assemble install
configure(webModule) {
//...
dependencies {
provided(servletApi)
compile(guiModule)
compile('com.haulmont.theme:halo-
facebook:0.1')
themes('com.haulmont.theme:halo-
facebook:0.1')
}
//...
}
5. Внесите следующие изменения в файл styles.scss:
6. Последним шагом будет добавление ссылки на halo-
facebook-theme.properties в файле web-
app.properties:
themes/
halo-facebook/
branding/
app-icon-login.png
app-icon-menu.png
com.company.application/
app-component.scss
halo-ext.scss
halo-ext-defaults.scss
favicon.ico
styles.scss
@import "halo-facebook-defaults";
@import "com.company.application/halo-ext-defaults";
@import "app-components";
@import "com.company.application/halo-ext";
.halo-facebook {
// include auto-generated app components SCSS
@include app_components;
@include com_company_application-halo-ext;
}
cuba.themeConfig = havana-theme.properties halo-
theme.properties /halo-facebook/halo-facebook-
theme.prope