Свернуть все | Развернуть все | Скрыть панель

Содержание

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

Версия 5.6    Более новая версия доступна в разделе документации.


Содержание

Предисловие
1. Введение
1.1. Обзор платформы
1.2. Технические требования
1.3. Release Notes
2. Установка и настройка инструментария
2.1. Установка CUBA Studio
2.2. Интеграция CUBA Studio с IDE
3. Быстрый старт
3.1. Описание задачи
3.2. Создание проекта
3.3. Создание сущностей
3.4. Создание таблиц базы данных
3.5. Создание экранов пользовательского интерфейса
3.5.1. Экраны управления Покупателями
3.5.2. Экраны управления Заказами
3.5.3. Меню приложения
3.5.4. Экран редактирования Покупателя со списком Заказов
3.6. Запуск приложения
4. Устройство платформы
4.1. Архитектура
4.1.1. Уровни и блоки приложения
4.1.2. Модули приложения
4.1.3. Базовые проекты
4.1.4. Состав приложения
4.2. Общие компоненты
4.2.1. Модель данных
4.2.1.1. Базовые классы сущностей
4.2.1.2. Аннотации сущностей
4.2.1.2.1. Аннотации класса
4.2.1.2.2. Аннотации атрибутов
4.2.1.3. Атрибуты типа enum
4.2.1.4. Мягкое удаление
4.2.1.4.1. Использование
4.2.1.4.2. Политика обработки связей
4.2.1.4.3. Ограничение уникальности на уровне БД
4.2.2. Metadata Framework
4.2.2.1. Интерфейсы метаданных
4.2.2.2. Формирование метаданных
4.2.2.3. Datatype
4.2.2.3.1. Пример форматирования даты в UI
4.2.2.3.2. Примеры форматирования дат и чисел в коде приложения
4.2.2.3.3. Пример специализированного Datatype
4.2.2.4. Мета-аннотации
4.2.3. Представления
4.2.3.1. Создание представлений
4.2.4. Управляемые бины
4.2.4.1. Создание бина
4.2.4.2. Использование бина
4.2.5. JMX-бины
4.2.5.1. Создание JMX-бина
4.2.5.2. JMX-бины платформы
4.2.5.2.1. CachingFacadeMBean
4.2.5.2.2. ConfigStorageMBean
4.2.5.2.3. EmailerMBean
4.2.5.2.4. PersistenceManagerMBean
4.2.5.2.5. ScriptingManagerMBean
4.2.5.2.6. ServerInfoMBean
4.2.6. Интерфейсы инфраструктуры
4.2.6.1. Configuration
4.2.6.2. Messages
4.2.6.2.1. MessageTools
4.2.6.3. Metadata
4.2.6.3.1. MetadataTools
4.2.6.4. Resources
4.2.6.5. Scripting
4.2.6.6. Security
4.2.6.7. TimeSource
4.2.6.8. UserSessionSource
4.2.6.9. UuidSource
4.2.6.10. DataManager
4.2.6.10.1. Запросы с distinct
4.2.6.10.2. Последовательная выборка
4.2.7. AppContext
4.2.8. Свойства приложения
4.2.8.1. Доступ к свойствам
4.2.8.2. Хранение свойств в файлах
4.2.8.3. Хранение свойств в базе данных
4.2.8.4. Конфигурационные интерфейсы
4.2.8.4.1. Использование
4.2.8.4.2. Типы свойств
4.2.8.4.3. Значения по умолчанию
4.2.9. Локализация сообщений
4.2.9.1. Пакеты сообщений
4.2.9.2. Главный пакет сообщений
4.2.9.3. Локализация названий сущностей и атрибутов
4.2.9.4. Локализация enum
4.2.10. Аутентификация пользователей
4.2.10.1. UserSession
4.2.10.2. Вход в систему
4.2.10.3. SecurityContext
4.2.11. Обработка исключений
4.2.11.1. Классы исключений
4.2.11.2. Передача исключений Middleware
4.2.11.3. Обработчики исключений клиентского уровня
4.3. Компоненты работы с базой данных
4.3.1. Типы СУБД
4.3.1.1. Поддержка произвольных СУБД
4.3.1.2. Версия СУБД
4.3.2. Скрипты создания и обновления БД
4.3.2.1. Структура SQL-скриптов
4.3.2.2. Структура Groovy-скриптов
4.3.3. Выполнение скриптов БД задачами Gradle
4.3.4. Выполнение скриптов БД сервером
4.4. Компоненты среднего слоя
4.4.1. Сервисы
4.4.1.1. Создание сервиса
4.4.1.2. Использование сервиса
4.4.1.3. DataService
4.4.2. Системная аутентификация
4.4.3. Интерфейс Persistence
4.4.3.1. PersistenceTools
4.4.3.2. PersistenceHelper
4.4.3.3. DbTypeConverter
4.4.4. Слой ORM
4.4.4.1. EntityManager
4.4.4.2. Состояния сущности
4.4.4.3. Загрузка по требованию
4.4.4.4. Выполнение JPQL запросов
4.4.4.4.1. Поиск подстроки без учета регистра
4.4.4.4.2. Макросы в JPQL
4.4.4.5. Выполнение SQL запросов
4.4.4.6. Entity Listeners
4.4.5. Управление транзакциями
4.4.5.1. Программное управление транзакциями
4.4.5.2. Декларативное управление транзакциями
4.4.5.3. Примеры взаимодействия транзакций
4.4.5.3.1. Откат вложенной транзакции
4.4.5.3.2. Чтение и изменение данных во вложенной транзакции
4.4.5.4. Таймаут транзакции
4.4.5.4.1. Особенности реализации для различных СУБД
4.5. Универсальный пользовательский интерфейс
4.5.1. Экраны
4.5.1.1. Типы экранов
4.5.1.1.1. Фрейм
4.5.1.1.2. Простой экран
4.5.1.1.3. Экран выбора
4.5.1.1.4. Экран редактирования
4.5.1.2. XML-дескриптор
4.5.1.3. Контроллер экрана
4.5.1.3.1. AbstractFrame
4.5.1.3.2. AbstractWindow
4.5.1.3.3. AbstractLookup
4.5.1.3.4. AbstractEditor
4.5.1.3.5. Инжекция зависимостей контроллеров
4.5.1.3.6. Компаньоны контроллеров
4.5.2. Библиотека визуальных компонентов
4.5.2.1. Компоненты
4.5.2.1.1. Button
4.5.2.1.2. Bulk Editor
4.5.2.1.3. CheckBox
4.5.2.1.4. DateField
4.5.2.1.5. Embedded
4.5.2.1.6. FieldGroup
4.5.2.1.7. FileMultiUploadField
4.5.2.1.8. FileUploadField
4.5.2.1.9. Filter
4.5.2.1.9.1. Использование фильтра
4.5.2.1.9.2. Описание компонента Filter
4.5.2.1.9.3. Права пользователей
4.5.2.1.9.4. Внешние параметры для управления фильтрами
4.5.2.1.9.5. Последовательное наложение фильтров
4.5.2.1.10. GroupTable
4.5.2.1.11. Label
4.5.2.1.12. Link
4.5.2.1.13. LinkButton
4.5.2.1.14. LookupField
4.5.2.1.15. LookupPickerField
4.5.2.1.16. MaskedField
4.5.2.1.17. OptionsGroup
4.5.2.1.18. PasswordField
4.5.2.1.19. PickerField
4.5.2.1.20. PopupButton
4.5.2.1.21. ProgressBar
4.5.2.1.22. Related Entities
4.5.2.1.23. RichTextArea
4.5.2.1.24. SearchPickerField
4.5.2.1.25. Table
4.5.2.1.26. TextArea
4.5.2.1.27. TextField
4.5.2.1.28. TimeField
4.5.2.1.29. TokenList
4.5.2.1.30. Tree
4.5.2.1.31. TreeTable
4.5.2.1.32. TwinColumn
4.5.2.2. Контейнеры
4.5.2.2.1. BoxLayout
4.5.2.2.2. ButtonsPanel
4.5.2.2.3. GridLayout
4.5.2.2.4. GroupBoxLayout
4.5.2.2.5. IFrame
4.5.2.2.6. ScrollBoxLayout
4.5.2.2.7. SplitPanel
4.5.2.2.8. TabSheet
4.5.2.3. Разное
4.5.2.3.1. Formatter
4.5.2.3.2. Presentation
4.5.2.3.3. Timer
4.5.2.3.4. Validator
4.5.2.4. XML-атрибуты компонентов
4.5.3. Источники данных
4.5.3.1. Создание источников данных
4.5.3.1.1. Декларативное создание
4.5.3.1.2. Программное создание
4.5.3.1.3. Собственные классы реализации
4.5.3.2. Запросы в CollectionDatasourceImpl
4.5.3.2.1. Возвращаемые значения
4.5.3.2.2. Параметры запроса
4.5.3.2.3. Фильтр запроса
4.5.3.2.4. Поиск подстроки без учета регистра
4.5.3.3. Слушатели источников данных
4.5.3.4. DsContext
4.5.3.5. DataSupplier
4.5.4. Действия. Интерфейс Action
4.5.4.1. Декларативное создание действий
4.5.4.2. Стандартные действия
4.5.4.2.1. Стандартные действия с коллекцией
4.5.4.2.1.1. CreateAction
4.5.4.2.1.2. EditAction
4.5.4.2.1.3. RemoveAction
4.5.4.2.1.4. RefreshAction
4.5.4.2.1.5. AddAction
4.5.4.2.1.6. ExcludeAction
4.5.4.2.1.7. ExcelAction
4.5.4.2.2. Стандартные действия поля выбора
4.5.4.2.2.1. LookupAction
4.5.4.2.2.2. ClearAction
4.5.4.2.2.3. OpenAction
4.5.4.3. BaseAction
4.5.5. Диалоговые окна и уведомления
4.5.5.1. Диалоговые окна
4.5.5.2. Уведомления
4.5.6. Фоновые задачи
4.5.6.1. Использование фоновых задач
4.5.6.2. Настройка окружения
4.5.7. Создание темы приложения
4.5.7.1. Тема в веб-приложениях
4.5.7.1.1. Использование существующих тем
4.5.7.1.2. Расширение существующей темы
4.5.7.1.3. Создание новой темы
4.5.7.2. Тема в десктоп-приложениях
4.5.8. Специфика Web Client
4.5.8.1. Работа с компонентами Vaadin
4.5.8.2. Компоновка главного окна приложения
4.5.9. Специфика Desktop Client
4.5.9.1. Работа с компонентами Swing
4.5.10. Создание собственных компонентов
4.5.10.1. Использование сторонних компонентов Vaadin
4.5.10.2. Интеграция компонентов в Generic UI
4.5.11. Горячие клавиши
4.6. Компоненты портала
4.6.1. Базовая функциональность
4.6.2. REST API
4.6.2.1. Включение в проект
4.6.2.2. Описание функций
4.6.2.2.1. Логин
4.6.2.2.2. Логаут
4.6.2.2.3. Загрузка экземпляра персистентного объекта из базы данных по идентификатору
4.6.2.2.4. Выполнение JPQL запроса для выборки данных
4.6.2.2.5. Коммит новых и измененных экземпляров, удаление
4.6.2.2.6. Загрузка файла из хранилища
4.6.2.2.7. Получение описания модели данных в формате HTML
4.6.2.2.8. Cоздание новых представлений на сервере
4.6.2.2.9. Вызов сервисов
4.6.2.2.9.1. Вызов сервиса с помощью GET запроса
4.6.2.2.9.2. Вызов сервиса с помощью POST запроса
4.6.2.2.9.3. Поддерживаемые типы параметров метода сервиса
4.6.2.2.9.4. Результат вызова сервиса
4.7. Механизмы платформы
4.7.1. Выполнение задач по расписанию
4.7.1.1. Spring TaskScheduler
4.7.1.2. Назначенные задания CUBA
4.7.1.2.1. Регистрация задания
4.7.1.2.2. Управление обработкой заданий
4.7.1.2.3. Особенности реализации
4.7.2. Отправка email
4.7.2.1. Методы отправки
4.7.2.2. Вложения
4.7.2.3. Настройка параметров отправки email
4.7.3. Динамические атрибуты
4.7.3.1. Управление динамическими атрибутами
4.7.3.2. Категоризируемые сущности
4.7.3.3. Динамические атрибуты в REST API
4.7.4. Пессимистичная блокировка
4.7.4.1. Блокировка редактирования сущностей
4.7.4.2. Блокировка произвольных процессов
4.7.4.3. Мониторинг блокировок
4.7.5. Статистика сущностей
4.7.6. Журнал изменений сущностей
4.7.6.1. Настройка журналирования
4.7.6.2. Отображение журнала
4.7.7. Снимки сущностей
4.7.7.1. Сохранение снимков
4.7.7.2. Отображение снимков
4.7.8. Хранилище файлов
4.7.8.1. Загрузка файлов
4.7.8.2. Выгрузка данных
4.7.8.3. Стандартная реализация хранилища
4.7.9. Генерация последовательностей
4.7.10. Выполнение SQL с помощью QueryRunner
4.7.11. Интеграция с MyBatis
4.7.12. Панель папок
4.7.12.1. Папки приложения
4.7.12.2. Папки поиска
4.7.12.3. Наборы
4.7.13. Ссылки на экраны
4.7.14. Инспектор сущностей
4.7.15. Информация об используемом ПО
4.8. Расширение функциональности
4.8.1. Расширение сущности
4.8.2. Расширение экранов
4.8.3. Расширение бизнес-логики
5. Разработка приложений
5.1. Рекомендуемый стиль кода
5.2. Файловая структура проекта
5.3. Описание скриптов сборки
5.3.1. Структура build.gradle
5.3.2. Запуск задач сборки
5.3.3. Сборка на сервере Continuous Integration
5.4. Создание проекта
5.5. Проектирование БД
5.5.1. Создание схемы БД
5.5.2. Подключение к HSQLDB внешними инструментами
5.5.2.1. Подключение с помощью Squirrel SQL
5.5.2.2. Подключение с помощью IntelliJ IDEA Ultimate
5.5.3. Особенности PostgreSQL
5.5.4. Особенности MS SQL Server
5.5.5. Особенности Oracle Database
5.6. Логгирование
5.6.1. Настройка логгирования в Tomcat
5.6.2. Настройка логгирования в десктоп клиенте
5.7. Отладка и тестирование
5.7.1. Подключение отладчика
5.7.2. Отладка виджетов в веб-браузере
5.7.3. Тестирование
5.7.3.1. Модульные тесты
5.7.3.2. Интеграционные тесты Middleware
5.7.3.3. Интеграционные тесты клиентского уровня
5.8. Рецепты разработки
5.8.1. Получение локализованных сообщений
5.8.2. Присвоение начальных значений
5.8.2.1. Инициализация полей сущности
5.8.2.2. Инициализация с помощью CreateAction
5.8.2.3. Использование метода initNewItem
5.8.3. Редактирование композитных сущностей
5.8.3.1. Реализация композиции
5.8.3.2. Глубокая композиция
5.8.4. Выполнение кода на старте приложения
5.8.5. Загрузка и вывод изображений
5.8.6. Создание собственных визуальных компонентов
5.8.6.1. Пример использования стороннего компонента Vaadin
5.8.6.2. Пример интеграции компонента Vaadin в Generic UI
6. Развертывание приложений
6.1. Каталоги приложения
6.1.1. Конфигурационный каталог
6.1.2. Рабочий каталог
6.1.3. Каталог журналов
6.1.4. Временный каталог
6.1.5. Каталог скриптов базы данных
6.2. Варианты развертывания
6.2.1. Быстрое развертывание в Tomcat
6.2.1.1. Использование Tomcat при эксплуатации приложения
6.2.2. Развертывание в WAR
6.3. Масштабирование приложения
6.3.1. Настройка кластера Web Client
6.3.1.1. Установка и настройка Load Balancer
6.3.1.2. Настройка серверов Web Client
6.3.2. Настройка кластера Middleware
6.3.2.1. Настройка обращения к кластеру Middleware
6.3.2.2. Настройка взаимодействия серверов Middleware
6.3.3. Server Id
6.4. Использование инструментов JMX
6.4.1. Встроенная JMX консоль
6.4.2. Настройка удаленного доступа к JMX
6.4.2.1. Tomcat JMX под Windows
6.4.2.2. Tomcat JMX под Linux
6.5. Создание и обновление БД при эксплуатации приложения
6.5.1. Использование механизма выполнения скриптов БД сервером
6.5.2. Инициализация и обновление БД из командной строки
6.6. Использование файла лицензии
7. Подсистема безопасности
7.1. Компоненты подсистемы безопасности
7.1.1. Окно входа в систему
7.1.2. Пользователи
7.1.2.1. Замещение пользователей
7.1.3. Часовой пояс
7.1.4. Разрешения
7.1.5. Роли
7.1.6. Группы доступа
7.1.6.1. Ограничения
7.1.6.2. Атрибуты сессии
7.1.7. Интеграция с LDAP
7.1.7.1. Базовая интеграция с Active Directory
7.1.7.2. Настройка аутентификации с использованием Jespa
7.1.7.2.1. Подключение библиотеки
7.1.7.2.2. Настройка конфигурации
7.2. Примеры управления доступом
7.2.1. Настройка ролей
7.2.2. Создание локальных администраторов
A. Конфигурационные файлы
A.1. context.xml
A.2. datatypes.xml
A.3. dispatcher-spring.xml
A.4. menu.xml
A.5. metadata.xml
A.6. permissions.xml
A.7. persistence.xml
A.8. remoting-spring.xml
A.9. screens.xml
A.10. spring.xml
A.11. views.xml
A.12. web.xml
B. Свойства приложения
C. Системные свойства
Основные определения и понятия

Предисловие

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

1. Целевая аудитория

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

  • Java Standard Edition

  • Реляционные базы данных (SQL, DDL)

2. Дополнительные материалы

Настоящее Руководство, а также другая документация по платформе CUBA, доступны по адресу www.cuba-platform.ru/manual.

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

3. Обратная связь

Если у Вас имеются предложения по улучшению данного руководства, обратитесь пожалуйста в службу поддержки по адресу ru.cuba-platform.com/support/topics.

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

Глава 1. Введение

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

1.1. Обзор платформы

Ключевые особенности

  • Использование платформы Java, а, следовательно, возможность работы под управлением практически любых операционных систем на серверах и рабочих станциях

  • Полностью открытый исходный код

  • Независимость от специфики СУБД

  • Создаваемые на базе платформы приложения легко могут быть развернуты в отказоустойчивой конфигурации

  • Наличие эффективных средств разработки пользовательского интерфейса с помощью только Java и XML

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

  • Встроенный механизм создания и генерации отчетов с выводом в форматы офисных документов и PDF (см. руководство Генератор отчетов)

  • Механизм создания и выполнения бизнес-процессов с интегрированным визуальным редактором процессов (см. руководство Подсистема Workflow)

  • Полнотекстовый поиск по атрибутам сущностей и по содержимому загруженных файлов (см. руководство Полнотекстовый поиск)

  • Возможность отображения диаграмм, в том числе, диаграммы Ганта (см. руководство Отображение диаграмм)

  • Встроенный REST API с передачей данных в форматах XML или JSON для быстрой интеграции со сторонними приложениями

  • Механизм создания расширений функциональности, позволяющий адаптировать тиражируемые продукты для конкретных заказчиков, не затрудняя при этом обновления версий продуктов

  • CUBA Studio - инструмент быстрой разработки на платформе. Studio включает в себя средства создания нового проекта, описания модели данных, визуальный редактор экранов и других элементов проекта. Использование Studio не исключает возможности программирования в классической Java IDE, тем самым достигается максимальная эффективность при работе над проектом с помощью обоих инструментов:

    • Studio используется для быстрого старта проекта, визуального создания модели данных и компоновки экранов UI

    • Java IDE используется для программирования бизнес-логики и обработки событий UI

    CUBA Studio содержит средства для взаимодействия с IntelliJ IDEA и Eclipse, позволяющие быстро переходить из Studio в IDE и обратно.

Преимущества использования платформы CUBA

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

  • Декларативный подход к созданию пользовательского интерфейса имеет следующие преимущества:

    • абстрагирует разработчика от сложностей разнородных технологий (HTML/JavaScript, Swing, и т.п.)

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

  • Созданные таким образом экраны одинаково работоспособны в обоих типах клиентов, поддерживаемых платформой: веб и десктоп, что позволяет с минимумом усилий создавать системы, использующие преимущества веб-интерфейса и/или настольных приложений

  • Платформой предоставляется готовая функциональность на следующих уровнях:

  • Функциональность платформы позволяет значительно сократить сроки разработки и удешевить проект

  • Для создания веб-клиента приложения на основе CUBA не обязательно хорошее знание классических веб-технологий: HTML, CSS, JavaScript

1.2. Технические требования

Минимальные требования для ведения разработки на платформе CUBA:

  • Оперативная память - 4 ГБ.

  • Место на жестком диске - 5 ГБ.

  • Операционная система - Microsoft Windows, Linux или Mac OS X.

1.3. Release Notes

Список изменений в платформе доступен по адресу files.cuba-platform.com/cuba/platform/platform-5.5-changelog.html.

Глава 2. Установка и настройка инструментария

Минимально необходимым набором программного обеспечения является:

  • Java SE Development Kit (JDK) 7 или 8. Рекомендуется использовать Oracle Java HotSpot VM.

    Для сборки и запуска проектов вне Studio в переменной окружения JAVA_HOME необходимо установить путь к корневому каталогу JDK, например C:\Program Files\Java\jdk1.8.0_45. Для Windows это можно сделать, открыв Компьютер -> Свойства системы -> Дополнительные параметры системы -> Дополнительно -> Переменные среды, и задав значение переменной в списке Системные переменные.

  • Cреда разработки на Java: IntelliJ IDEA Community Edition 12+ или Eclipse 4.3+. Рекомендуется использовать IntelliJ IDEA.

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

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

2.1. Установка CUBA Studio

Окружение:

  • Убедитесь в наличии на компьютере Java SE Development Kit (JDK) 7 или 8, выполнив в консоли команду

    java -version

    В ответ должно быть выведено сообщение с номером версии Java, например 1.8.0_45.

  • Если для соединения с интернетом используется прокси-сервер, в JVM, исполняющие Studio и Gradle, необходимо передавать специальные системные свойства Java. Они описаны в документе http://docs.oracle.com/javase/7/docs/technotes/guides/net/proxies.html (см. свойства для протоколов HTTP и HTTPS).

    Рекомендуется установить нужные свойства в переменной окружения JAVA_OPTS. Скрипт запуска Studio передает JAVA_OPTS в java.exe.

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

  1. Загрузите архив studio-<version>.zip со страницы www.cuba-platform.ru/download.

  2. Распакуйте архив в локальный каталог, например, c:/work/studio

  3. Откройте командную строку, перейдите в подкаталог bin и запустите

    studio

  4. В окне CUBA Studio Server введите следующие параметры:

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

    • Gradle home − оставьте поле пустым, в этом случае при первом запуске будет автоматически загружен нужный дистрибутив Gradle.

      Если по какой-либо причине Вы хотите использовать уже установленный на компьютере Gradle, введите в поле путь к соответствующему каталогу. Для работы системы сборки проектов требуется Gradle версии 1.12.

    • Server port − порт, на котором будет запущен сервер CUBA Studio (по умолчанию 8111)

    • IDE port − порт, на котором принимает подключения плагин IDE (по умолчанию 48561)

    • Repository − URL и параметры аутентификации репозитория бинарных артефактов.

    Также доступны следующие опции:

    • Check for updates - проверять наличие новых версий при старте.

    • Help language - язык встроенной справки.

    • Offline - включить возможность работы без интернет-соединения при условии, что все необходимые библиотеки были предварительно загружены из репозитория.

    • Send anonymous statistics and crash reports - разрешить Studio отправлять статистику ошибок разработчикам.

    • Enable remote connection - по умолчанию считается, что Studio работает на локальном хосте. Установите флажок, если вам нужна возможность подкючения к этой копии Studio с удаленного хоста.

  5. Запустите сервер Studio, нажав кнопку Start.

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

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

  6. Запустите веб-браузер и перейдите по указанному адресу.

  7. В веб-интерфейсе Studio нажмите кнопку Open project. В открывшемся окне Select project нажмите New для создания нового проекта, или Import для добавления имеющегося проекта в список Studio.

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

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

Для интеграции с IntelliJ IDEA или Eclipse выполните следующие шаги:

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

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

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

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

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

    2. В IntelliJ IDEA в меню Settings в группе Languages and Frameworks найдите пункт CUBA. На панели Studio integration установите флажок Enabled и нажмите на кнопку OK.

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

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

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

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

Глава 3. Быстрый старт

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

На Вашей рабочей машине уже должно быть установлено и настроено необходимое программное обеспечение, см. Глава 2, Установка и настройка инструментария.

Основные задачи, стоящие при разработке нашего приложения:

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

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

3.1. Описание задачи

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

Покупатель имеет следующие характеристики:

  • Имя

  • Электронная почта

Характеристики заказа:

  • Принадлежность покупателю

  • Дата

  • Сумма

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

  • Окно списка покупателей;

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

  • Окно общего списка заказов;

  • Окно редактирования заказа.

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

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

  1. Запустите CUBA Studio и откройте ее веб-интерфейс (см. Раздел 2.1, «Установка CUBA Studio»).

  2. В стартовом окне нажмите на кнопку Open project.

  3. В отобразившемся окне Select project нажмите на кнопку New.

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

  5. В полях ниже автоматически сгенерируются:

    • Project path − путь к каталогу нового проекта. Каталог можно выбрать вручную, нажав на кнопку ... рядом с полем. Отобразится окно Folder select со списком папок на жестком диске. Вы можете выбрать одну из них или создать новый каталог, нажав на кнопку +.

    • Project namespace - пространство имен, которое будет использоваться как префикс имен сущностей и таблиц базы данных. Пространство имен может состоять только из латинских букв, и должно быть как можно короче. Например, если имя проекта - sales_2, то пространство имен может быть sales или sal.

    • Root package − корневой пакет Java-классов. Может быть скорректирован позже, однако сгенерированные на этапе создания классы перемещены не будут.

    • Base projects version - используемая в проекте версия платформы. Артефакты платформы будут автоматически загружены из репозитория при сборке проекта.

      Оставим все предложенные значения без изменений.

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

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

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

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

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

    Логин и пароль пользователя − admin / admin.

    Запущенное приложение содержит два главных пункта меню (Администрирование и Помощь), функциональность подсистемы безопасности и администрирования системы.

3.3. Создание сущностей

Создадим класс сущности Покупатель (Customer).

  • Перейдите на вкладку Entities на панели навигатора и нажмите на кнопку New entity. Появится диалоговое окно New entity.

  • В поле Class name введите название класса сущности − Customer.

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

  • В полях Name и Table автоматически сгенерируются имя сущности и имя таблицы в базе данных.

  • В поле Parent class оставьте установленное значение − StandardEntity.

  • Поле Inheritance strategy оставьте пустым.

  • Нажмите на кнопку рядом с полем Name. На экране отобразится окно Localized message, в нем следует задать локализацию имени сущности на доступных языках.

Далее создадим атрибуты сущности. Для этого нажмите на кнопку New, находящуюся под таблицей Attributes.

  • В отобразившемся окне Create attribute в поле Name введите название атрибута сущности − name, в списке Attribute type выберите значение DATATYPE, в поле Type укажите тип атрибута String и далее укажите длину текстового атрибута в поле Length, равной 100 символам. Установите флажок Mandatory. В поле Column автоматически сгенерируется имя колонки таблицы в базе данных.

    Далее нажмите на кнопку рядом с названием атрибута. На экране отобразится окно Localized message, в нем следует задать локализацию названия атрибута на доступных языках.

    Для добавления атрибута нажмите на кнопку Add.

  • Атрибут email создается таким же образом, за исключением того, что в поле Length следует указать значение 50.

После создания атрибутов перейдите на вкладку Instance name дизайнера сущности для задания Name pattern. В списке Available attributes выделите атрибут name и перенесите его в список Name pattern attributes нажав на кнопку с изображением стрелки вправо.

На этом создание сущности Customer завершено. Нажмите на кнопку OK в верхнем левом углу дизайнера сущности для сохранения изменений.

Создадим сущность Заказ (Order). В панели Entities нажмите на кнопку New entity. В поле Class name введите название класса сущности − Order. Сущность должна иметь следующие атрибуты:

  • Namecustomer, Attribute typeASSOCIATION, TypeCustomer, CardinalityMANY_TO_ONE.

  • Namedate, Attribute typeDATATYPE, TypeDate. Для атрибута date установите флажок Mandatory.

  • Nameamount, Attribute typeDATATYPE, TypeBigDecimal.

Для каждого атрибута укажите локализованные названия нажимая на кнопку рядом с именем атрибута.

3.4. Создание таблиц базы данных

Для создания таблиц базы данных достаточно на вкладке Entities панели навигатора нажать на кнопку Generate DB scripts. После этого откроется страница Database scripts. На вкладке будут сгенерированы скрипты обновления базы данных от ее текущего состояния (Update scripts) и скрипты создания базы данных с нуля (Init tables, Init constraints, Init data). Также на вкладке будут доступны уже выполненные скрипты обновления базы данных, если они есть.

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

3.5. Создание экранов пользовательского интерфейса

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

3.5.1. Экраны управления Покупателями

Для создания стандартных экранов просмотра и редактирования Покупателей необходимо выделить сущность Customer на вкладке Entities панели навигатора и нажать на кнопку Create standard screens внизу панели. После этого на экране отобразится окно Create standard screens.

Все поля этого окна заполнены значениями по умолчанию, менять их не нужно. Нажмите на кнопку Create.

Во вкладке Screens панели навигатора в модуле GUI Module появятся элементы customer-edit.xml и customer-browse.xml.

Для экранов можно задать локализацию заголовков. Для этого выделите один из файлов и нажмите на кнопку Edit. Отобразится страница дизайнера экрана. Перейдите на вкладку Properties. Нажмите на кнопку рядом с полем Caption и задайте локализованные заголовки экрана. Повторите те же действия для другого экрана. Для редактирования всех локализованных сообщений экранов сразу можно воспользоваться элементом messages.properties, расположенным в том же пакете, что и экраны. Выделите его и нажмите Edit, в появившемся редакторе задайте сообщения browseCaption и editCaption на доступных языках.

3.5.2. Экраны управления Заказами

Сущность Заказ (Order) имеет следующую особенность: так как среди прочих атрибутов существует ссылочный атрибут Order.customer, требуется определить представление сущности Order, включающее этот атрибут (стандартное представление _local не включает ссылочных атрибутов).

Для этого перейдите на вкладку Entities на панели навигатора, выделите сущность Order и нажмите на кнопку New view. Отобразится страница дизайнера представлений. В качестве имени введите orderWithCustomer, в списке атрибутов нажмите на атрибут customer и на отобразившейся справа панели выберите представление _minimal для сущности Customer.

Нажмите на кнопку OK в верхнем левом углу.

Далее выделите сущность Order и нажмите на кнопку Create standard screens. В отобразившемся окне Create standard screens в качестве Browse view и Edit view выберите значение orderWithCustomer и нажмите на кнопку Create.

Во вкладке Screens панели навигатора в модуле GUI Module появятся элементы order-edit.xml и order-browse.xml.

Вы можете задать локализованные заголовки экранов аналогично описанному для экранов Покупателя.

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

При создании экраны были добавлены в пункт меню application, имеющийся по умолчанию. Переменуем его. Для этого перейдите на вкладку Main menu на панели навигатора и нажмите на кнопку Edit. Отобразится страница дизайнера меню. Выделите пункт меню application для просмотра его свойств.

В поле Id введите новое значение идентификатора меню − shop, нажмите на кнопку Caption edit и задайте локализованное название пункта меню.

После редактирования меню нажмите на кнопку OK в верхнем левом углу рабочей панели.

3.5.4. Экран редактирования Покупателя со списком Заказов

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

  • Перейдите на вкладку Screens на панели навигатора. Выделите экран customer-edit.xml и нажмите на кнопку Edit.

  • На странице дизайнера экрана перейдите на вкладку Datasources и нажмите на кнопку New.

  • Выделите только что созданный источник данных в списке. В правой части страницы отобразятся его характеристики.

  • В поле Type укажите collectionDatasource.

  • В поле Id введите значение идентификатора источника данных − ordersDs.

  • В списке Entity выберите сущность com.sample.sales.entity.Order.

  • В списке View выберите представление _local.

  • В поле Query введите следующий запрос:

    select o from sales$Order o where o.customer.id = :ds$customerDs order by o.date

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

  • Нажмите на кнопку Apply для сохранения изменений.

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

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

  • Перетащите компонент Table из палитры компонентов на панель иерархии компонентов между label и windowActions. Выделите компонент в иерархии и на панели свойств на вкладке Layout задайте размеры таблицы: в поле width укажите 100%, в поле height установите значение 200px. Перейдите на вкладку Properties. В качестве идентификатора укажите значение ordersTable, из списка доступных источников данных выберите orderDs.

    Далее нажмите на кнопку edit, относящуюся к columns. На экране отобразится диалоговое окно управления колонками таблицы. В первой строке в колонке id из выпадающего списка выберите значение date, во второй строке − amount.

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

3.6. Запуск приложения

Посмотрим, как созданные нами экраны выглядят в работающем приложении. Для этого выполните Run > Restart application.

Зайдите в систему, выбрав русский язык в окне логина. Откройте пункт меню Продажи > Покупатели:

Рисунок 1. Экран списка Покупателей

Экран списка Покупателей


Нажмите на кнопку Создать:

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

Экран редактирования Покупателя


Откройте пункт меню Продажи > Заказы:

Рисунок 3. Экран списка Заказов

Экран списка Заказов


Нажмите на кнопку Создать:

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

Экран редактирования Заказа


Глава 4. Устройство платформы

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

4.1. Архитектура

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

4.1.1. Уровни и блоки приложения

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

В дальнейшем речь пойдет в основном о среднем слое и клиентах, поэтому для краткости выражение "все уровни" означает два этих уровня.

На каждом уровне возможно создание одного или нескольких блоков (units) приложения. Блок представляет собой обособленную исполняемую программу, взаимодействующую с другими блоками приложения. Средства платформы CUBA позволяют создавать блоки в виде веб-приложений и десктопных приложений. Разработка блоков для мобильных платформ на данный момент остается за рамками CUBA, однако такие блоки, созданные другими средствами, могут быть интегрированы со стандартными блоками приложения.

Рисунок 5. Уровни и блоки приложения

Уровни и блоки приложения

Middleware

Средний слой, содержащий основную бизнес-логику приложения и выполняющий обращения к базе данных. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. См. Раздел 4.4, «Компоненты среднего слоя»

Web Client

Основной блок клиентского уровня. Содержит интерфейс, предназначенный, как правило, для внутренних пользователей организации. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. Реализация пользовательского интерфейса основана на фреймворке Vaadin. См. Раздел 4.5, «Универсальный пользовательский интерфейс»

Desktop Client

Дополнительный блок клиентского уровня. Содержит интерфейс, предназначенный, как правило, для внутренних пользователей организации. Представляет собой десктопное Java-приложение, реализация пользовательского интерфейса основана на фреймворке Java Swing. См. Раздел 4.5, «Универсальный пользовательский интерфейс»

Web Portal

Дополнительный блок клиентского уровня. Содержит интерфейс для внешних пользователей и средства интеграции с мобильными устройствами и сторонними приложениями. Представляет собой отдельное веб-приложение под управлением стандартного контейнера Java EE Web Profile. Реализация пользовательского интерфейса основана на фреймворке Spring MVC. См. Раздел 4.6, «Компоненты портала»

Обязательным блоком любого приложения является средний слой - Middleware. Для реализации пользовательского интерфейса, как правило, используется один или несколько клиентских блоков, например, Web Client и Web Portal.

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

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

4.1.2. Модули приложения

Модуль – наименьшая структурная единица CUBA-приложения. Представляет собой один модуль проекта приложения и соответствующий ему JAR файл с исполняемым кодом.

Стандартные модули:

  • global – включает в себя классы сущностей, интерфейсы сервисов и другие общие для всех уровней классы. Используется во всех блоках приложения.

  • core – реализация сервисов и всех остальных компонентов среднего слоя. Используется только на Middleware.

  • gui – общие компоненты универсального пользовательского интерфейса. Используется в Web Client и Desktop Client.

  • web – реализация универсального пользовательского интерфейса на Vaadin, а также другие специфичные для веб-клиента классы. Используется в блоке Web Client.

  • desktop – опциональный модуль – реализация универсального пользовательского интерфейса на Java Swing, а также другие специфичные для десктоп-клиента классы. Используется в блоке Desktop Client.

  • portal – опциональный модуль – реализация веб-портала на Spring MVC.

Рисунок 6. Модули приложения

Модули приложения

4.1.3. Базовые проекты

Функциональность платформы разделена на несколько так называемых базовых проектов:

  • cuba – основной базовый проект, содержит всю функциональность, описанную в данном руководстве, плюс подсистему безопасности (управление пользователями и их доступом к данным)

  • reports – подсистема генерации отчетов

  • workflow – подсистема управления потоками работ со встроенным визуальным редактором бизнес-процессов

  • fts – подсистема полнотекстового поиска

  • charts – подсистема вывода диаграмм

  • ccpayments – подсистема работы с кредитными картами

  • bpmn – механизм исполнения бизнес-процессов по стандарту BPMN 2.0

Создаваемое на основе платформы приложение может включать в себя функциональность базовых проектов путем объявления зависимостей от их артефактов. Зависимость от артефактов cuba является обязательной. Опциональные базовые проекты в свою очередь также зависят от cuba, и в принципе могут содержать зависимости между собой.

Рисунок 7. Зависимости между проектами

Зависимости между проектами

Сплошными линиями изображены обязательные зависимости, пунктирными − опциональные.

4.1.4. Состав приложения

Вышеописанные архитектурные принципы напрямую отражаются на составе собранного приложения. Рассмотрим его на примере простого приложения sales, которое имеет 2 блока – Middleware и Web Client; и включает в себя функциональность базовых проектов cuba и reports.

Рисунок 8. Состав простого приложения

Состав простого приложения

На рисунке изображено содержимое некоторых каталогов сервера Tomcat с развернутым в нем приложением sales.

Блок Middleware реализован веб-приложением app-core, блок Web Client – веб-приложением app. Исполняемый код веб-приложений содержится в каталогах WEB-INF/lib в наборе JAR-файлов. Каждый JAR представляет собой результат сборки (артефакт) одного из модулей приложения или базового проекта.

Например, состав JAR-файлов веб-приложения среднего слоя app-core определяется тем, что блок Middleware состоит из модулей global и core, и приложение использует базовые проекты cuba и reports (в данном случае версии 4.0.0).

4.2. Общие компоненты

В данной главе рассмотрены компоненты платформы, общие для всех уровней приложения.

4.2.1. Модель данных

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

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

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

  • неперсистентные – экземпляры существуют только в оперативной памяти

Сущности характеризуются своими атрибутами. Атрибут соответствует полю класса и паре методов доступа (get / set) к полю. Чтобы атрибут был неизменяемым (read only), достаточно не создавать метод set.

Персистентные сущности могут включать в себя атрибуты, не хранящиеся в БД. В случае неперсистентного атрибута можно не создавать поле класса, ограничившись методами доступа.

Класс сущности должен удовлетворять следующим требованиям:

  • наследоваться от одного из базовых классов, предоставляемых платформой (см. ниже)

  • иметь набор полей и методов доступа, соответствующих атрибутам сущностей

  • класс и его поля (или методы доступа при отсутствии для атрибута соответствующего поля) должны быть определенным образом аннотированы для работы JPA (в случае персистентной сущности) и фреймворка метаданных

  • для поддержки возможного расширения сущностей поля класса необходимо объявлять с модификатором protected, а не private

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

  • 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

  • сущность

Базовые классы сущностей (см. ниже) переопределяют equals() и hashCode() таким образом, что экземпляры сущностей сравниваются по их глобальным уникальным идентификаторам (UUID). То есть экземпляры считаются равными, если равны их идентификаторы. Глобальный уникальный идентификатор присваивается сразу после создания экземпляра в памяти, поэтому новые экземпляры также можно сравнивать и помещать в коллекции.

4.2.1.1. Базовые классы сущностей

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

Рисунок 9. Базовые классы сущностей

Базовые классы сущностей

  • Instance – декларирует базовые методы работы с объектами предметной области:

    • Получение глобального уникального идентификатора (UUID) сущности.

    • Получение ссылки на мета-класс объекта.

    • Генерация имени экземпляра.

    • Чтение/установка значений атрибутов по имени.

    • Добавление слушателей, получающих уведомления об изменениях атрибутов.

  • Entity – дополняет Instance понятием идентификатора сущности (который не обязательно равен UUID), причем Entity не определяет тип идентификатора, оставляя эту возможность наследникам.

  • AbstractInstance – реализует логику работы со слушателями изменения атрибутов.

    AbstractInstance хранит слушателей в коллекции WeakReference, т.е. при отсутствии внешних ссылок на добавленного слушателя, он будет немедленно уничтожен сборщиком мусора. Как правило, слушателями изменения атрибутов являются визуальные компоненты и источники данных UI, на которые всегда имеются ссылки из других объектов, поэтому проблема исчезновения слушателей не возникает. Однако если слушатель создается прикладным кодом и на него никто не ссылается естественным образом, необходимо кроме добавления в Instance сохранить его в некотором поле объекта.

  • AbstractNotPersistentEntity – базовый класс неперсистентных сущностей с идентификаторами типа UUID.

  • BaseEntity – базовый интерфейс всех персистентных сущностей, декларирует методы получения информации о том, кто и когда создал экземпляр сущности в базе данных.

  • BaseGenericIdEntity - реализует BaseEntity и добавляет аннотации для поддержки JPA, не специфицируя при этом тип идентификатора (то есть первичного ключа) сущности.

  • BaseUuidEntity - расширяет BaseGenericIdEntity, задавая атрибут-идентификатор с именем id типа UUID.

  • BaseLongIdEntity - расширяет BaseGenericIdEntity, задавая атрибут-идентификатор с именем id типа Long.

  • BaseIntegerIdEntity - расширяет BaseGenericIdEntity, задавая атрибут-идентификатор с именем id типа Integer.

  • BaseStringIdEntity - расширяет BaseGenericIdEntity, задавая только тип идентификатора - String. В конкретном классе сущности, унаследованной от BaseStringIdEntity, необходимо задать атрибут-идентификатор типа String и добавить ему JPA-аннотацию @Id.

  • Versioned – интерфейс сущностей, поддерживающих оптимистичную блокировку

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

  • SoftDelete – интерфейс сущностей, поддерживающих мягкое удаление

  • StandardEntity – наиболее часто используемый базовый класс персистентных сущностей, имеющий идентификатор типа UUID и реализующий интерфейсы Versioned, Updatable, SoftDelete.

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

  • Если сущность не хранится в БД, наследуйте ее от AbstractNotPersistentEntity.

  • Если сущность встраиваемая - наследуйте ее от EmbeddableEntity.

  • Если сущность только создается в БД, никогда не изменяется, и мягкое удаление не требуется - наследуйте ее от BaseUuidEntity.

  • Если сущность ведет себя стандартным образом: изменяется в БД, требует оптимистичной блокировки и мягкого удаления − наследуйте ее от StandardEntity.

  • В противном случае наследуйте сущность от BaseUuidEntity и реализуйте в классе тот набор интерфейсов Versioned, Updatable, SoftDelete, который требуется.

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

4.2.1.2. Аннотации сущностей

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

Аннотации пакета javax.persistence обеспечивают работу JPA, аннотации пакетов com.haulmont.* предназначены для управления метаданными и другими механизмами платформы.

Если для аннотации указано только простое имя класса, подразумевается что это класс платформы, расположенный в одном из пакетов com.haulmont.*

4.2.1.2.1. Аннотации класса
@javax.persistence.Entity

Объявляет класс сущностью модели данных.

Параметры:

  • name - имя сущности, обязательно должно начинаться с префикса, отделенного знаком $. Желательно использовать в качестве префикса короткое имя проекта для формирования отдельного пространства имен.

Пример:

@Entity(name = "sales$Customer")
@javax.persistence.MappedSuperclass

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

@javax.persistence.Table

Определяет таблицу базы данных для данной сущности.

Параметры:

  • name - имя таблицы

Пример:

@Table(name = "SALES_CUSTOMER")
@javax.persistence.Embeddable

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

Для задания имени сущности требуется применение аннотации @MetaClass .

@javax.persistence.Inheritance

Определяет стратегию наследования для иерархии классов сущностей. Данная аннотация должна быть помещена на корневом классе иерархии.

Параметры:

  • strategy - стратегия, по умолчанию SINGLE_TABLE

@javax.persistence.DiscriminatorColumn

Используется для определения колонки БД, отвечающей за различение типов сущностей в случае стратегий наследования SINGLE_TABLE и JOINED.

Параметры:

  • name - имя колонки-дискриминатора

  • discriminatorType - тип данных колонки-дискриминатора

Пример:

@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
@javax.persistence.DiscriminatorValue

Определяет значение колонки-дискриминатора для данной сущности. Эта аннотация должна быть помещена на конкретном классе сущности.

Пример:

@DiscriminatorValue("0")
@javax.persistence.PrimaryKeyJoinColumn

Используется в случае стратегии наследования JOINED для указания колонки внешнего ключа данной сущности, ссылающегося на первичный ключ сущности-предка.

Параметры:

  • name - имя колонки внешнего ключа данной сущности

  • referencedColumnName - имя колонки первичного ключа сущности предка

Пример:

@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
@NamePattern

Определяет способ получения имени экземпляра, возвращаемого методом Instance.getInstanceName().

Значением аннотации должна быть строка вида {0}|{1}, где

  • {0} - строка форматирования по правилам String.format(), или имя метода данного объекта с префиксом #. Метод должен возвращать String и не иметь параметров.

  • {1} - разделенный запятыми список имен полей класса, соответствующий формату {0}. В случае использования в {0} метода список полей все равно необходим, так как по нему формируется представление _minimal.

Примеры:

@NamePattern("%s|name")
@NamePattern("#getCaption|login,name")
@Listeners

Определяет список слушателей, предназначенных для реакции на события жизненного цикла экземпляров сущности на уровне Middleware.

Значением аннотации должна быть строка или массив строк с именами классов слушателей - см. Раздел 4.4.4.6, «Entity Listeners»

Строки используются здесь вместо ссылок на классы потому, что классы слушателей находятся только на уровне Middleware и не доступны клиентскому коду, в то время как классы самих сущностей используются на всех уровнях.

Примеры:

@Listeners("com.haulmont.cuba.security.listener.UserEntityListener")
@Listeners({"com.abc.sales.entity.FooListener","com.abc.sales.entity.BarListener"})
@MetaClass

Используется для объявления неперсистентной или встраиваемой сущности (т.е. когда аннотация @javax.persistence.Entity не применима)

Параметры:

  • name - имя сущности, обязательно должно начинаться с префикса, отделенного знаком $. Желательно использовать в качестве префикса короткое имя проекта для формирования отдельного пространства имен.

Пример:

@MetaClass(name = "sys$LockInfo")
@SystemLevel

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

@EnableRestore

Указывает, что экземпляры данной сущности доступны для восстановления после мягкого удаления в специальном экране core$Entity.restore.

@TrackEditScreenHistory

Указывает, что для данной сущности будет запоминаться история открытия экранов редактирования ({имя_сущности}.edit) с возможностью отображения в специальном экране sec$ScreenHistory.browse.

@Extends

Указывает, что данная сущность является расширением и должна повсеместно использоваться вместо базовой. См. Раздел 4.8, «Расширение функциональности»

@PostConstruct

Данная аннотация может быть указана для метода класса. Такой метод будет вызван сразу после создания экземпляра сущности через Metadata.create(). Это удобно, если для инициализации экземпляра сущности требуется вызов каких-либо бинов. Пример см. в Раздел 5.8.2.1, «Инициализация полей сущности».

4.2.1.2.2. Аннотации атрибутов

Аннотации атрибутов устанавливаются на соответствующие поля класса, за одним исключением: если требуется объявить неизменяемый (read only) неперсистентный атрибут foo, то достаточно создать метод доступа getFoo() и поместить на этот метод аннотацию @MetaProperty.

@javax.persistence.Transient

Указывает, что данное поле не хранится в БД, т.е. является неперсистентным.

Поля поддерживаемых JPA типов (см. http://docs.oracle.com/javaee/5/api/javax/persistence/Basic.html) по умолчанию являются персистентными, поэтому аннотация @Transient обязательна для объявления неперсистентного атрибута такого типа.

Для включения @Transient атрибута в метаданные, необходимо также указать аннотацию @MetaProperty .

@org.apache.openjpa.persistence.Persistent

Указывает, что данное поле хранится в БД, т.е. является персистентным.

Данная аннотация требуется только для нестандартного для JPA типа поля, платформа на данный момент поддерживает один такой тип - java.util.UUID. Таким образом, @Persistent требуется только в одном случае - при объявлении персистентного поля типа UUID.

@javax.persistence.Column

Определяет колонку БД, в которой будут храниться значения данного атрибута.

Параметры:

  • name - имя колонки

  • length - (необязательный параметр, по умолчанию 255) - длина колонки. Используется также при формировании метаданных и, в конечном счете, может ограничивать максимальную длину вводимого текста в визуальных компонентах, работающих с данным атрибутом. Для отмены ограничения по длине атрибуту необходимо добавить аннотацию @Lob.

  • nullable - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании nullable = false JPA контролирует наличие значения поля при сохранении, кроме того, визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

@javax.persistence.Id

Указывает, что данный атрибут является первичным ключом сущности. Обычно эта аннотация присутствует на поле базового класса, такого как BaseUuidEntity. Использовать эту аннотацию в конкретном классе сущности необходимо только при наследовании от базового класса BaseStringIdEntity (то есть при создании сущности со строковым первичным ключом).

@javax.persistence.ManyToOne

Определяет атрибут-ссылку на сущность с типом ассоциации много-к-одному.

Параметры:

  • fetch - (по умолчанию EAGER) параметр, определяющий, будет ли JPA жадно загружать ассоциированную сущность. Данный параметр всегда должен быть установлен в значение LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • optional - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании optional = false JPA контролирует наличие ссылки при сохранении, кроме того, визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

Например, несколько экземпляров Order (заказов) ссылаются на один экземпляр Customer (покупателя), в этом случае класс Order должен содержать следующее объявление:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@javax.persistence.OneToMany

Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации один-ко-многим.

Параметры:

  • mappedBy - поле связанной сущности, определяющее ассоциацию

  • targetEntity - тип связанной сущности. Необязательный параметр, если коллекция объявлена с использованием Java generics.

  • fetch - (необязательный параметр, по умолчанию LAZY) - определяет, будет ли JPA жадно загружать коллекцию связанных сущностей. Необходимо всегда оставлять значение по умолчанию LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • cascade - (необязательный параметр, по умолчанию {}) - каскадирование операций определяет, какие операции над сущностью должны быть применены к ассоциированным сущностям. Каскадирование на данном уровне не рекомендуется использовать.

Например, несколько экземпляров Item (пунктов заказа) ссылаются на один экземпляр Order (заказ) с помощью @ManyToOne поля Item.order, в этом случае класс Order может содержать коллекцию экземпляров Item:

@OneToMany(mappedBy = "order")
protected Set<Item> items;
@javax.persistence.OneToOne

Определяет атрибут-ссылку на сущность с типом ассоциации один-к-одному.

Параметры:

  • fetch - (по умолчанию EAGER) параметр, определяющий, будет ли JPA жадно загружать ассоциированную сущность. Данный параметр всегда должен быть установлен в значение LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • mappedBy - поле связанной сущности, определяющее ассоциацию. Требуется устанавливать только на ведомой стороне ассоциации.

  • optional - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании optional = false JPA контролирует наличие ссылки при сохранении, кроме того, визуальные компоненты, работающих с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

Пример ведущей стороны ассоциации, класс Driver:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CALLSIGN_ID")
protected DriverCallsign callsign;

Пример ведомой стороны ассоциации, класс DriverCallsign:

@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign")
protected Driver driver;
@javax.persistence.ManyToMany

Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации много-ко-многим.

Ассоциация много-ко-многим всегда имеет ведущую сторону и может иметь обратную сторону - ведомую. На ведущей стороне указывается дополнительная аннотация @JoinTable, на ведомой стороне - параметр mappedBy.

Параметры:

  • mappedBy - поле связанной сущности, определяющее ассоциацию с ведущей стороны. Необходимо указывать только на ведомой стороне.

  • targetEntity - тип связанной сущности. Необязательный параметр, если коллекция объявлена с использованием Java generics.

  • fetch - (необязательный параметр, по умолчанию LAZY) - определяет, будет ли JPA жадно загружать коллекцию связанных сущностей. Необходимо всегда оставлять значение по умолчанию LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

@javax.persistence.JoinColumn

Используется для указания колонки БД, определяющей ассоциацию между сущностями.

Параметры:

  • name - имя колонки

Пример:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@javax.persistence.JoinTable

Используется для указания таблицы связи на ведущей стороне @ManyToMany ассоциации.

Параметры:

  • name - имя таблицы связи

  • joinColumns - элемент @JoinColumn, определяющий колонку таблицы связей, соответствующую первичному ключу ведущей стороны ассоциации (т.е. содержащей аннотацию @JoinTable)

  • inverseJoinColumns - элемент @JoinColumn, определяющий колонку таблицы связей, соответствующую первичному ключу ведомой стороны ассоциации

Пример атрибута customers класса Group, являющегося ведущей стороной ассоциации:

@ManyToMany
@JoinTable(name = "SALES_CUSTOMER_GROUP_LINK",
  joinColumns = @JoinColumn(name = "GROUP_ID"),
  inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID"))
protected Set<Customer> customers;

Пример атрибута groups класса Customer, являющегося ведомой стороной этой же ассоциации:

@ManyToMany(mappedBy = "customers")
protected Set<Group> groups;
@javax.persistence.OrderBy

Определяет порядок элементов в атрибуте-коллекции на момент извлечения из базы данных. Данную аннотацию необходимо задавать для упорядоченных коллекций, таких как List или LinkedHashSet для получения предсказуемого порядка следования элементов.

Параметры:

  • value - строка, определяющая порядок, в формате:

    orderby_list::= orderby_item [,orderby_item]*
    orderby_item::= property_or_field_name [ASC | DESC]

Пример:

@OneToMany(mappedBy = "user")
@OrderBy("createTs")
protected List<UserRole> userRoles;
@javax.persistence.Embedded

Определяет атрибут типа встраиваемой сущности, в свою очередь аннотированной @Embeddable.

Пример:

@Embedded
protected Address address;
@javax.persistence.Temporal

Для атрибута типа java.util.Date уточняет тип хранимого значения: дата, время или дата+время.

Параметры:

  • value - тип хранимого значения: DATE, TIME, TIMESTAMP

Пример:

@Column(name = "START_DATE")
@Temporal(TemporalType.DATE)
protected Date startDate;
@javax.persistence.Version

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

Применение такого поля необходимо при реализации классом сущности интерфейса Versioned (базовый класс StandardEntity уже содержит такое поле).

Пример:

@Version
@Column(name = "VERSION")
private Integer version;
@javax.persistence.Lob

Указывает, что данный атрибут не имеет ограничений длины. Применяется совместно с аннотацией @Column. Если @Lob указан, то длина, заданная в @Column явно или по умолчанию, игнорируется.

Пример:

@Column(name = "DESCRIPTION")
@Lob
private String description;
@MetaProperty

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

Данная аннотация не обязательна для полей, снабженных следующими аннотациями пакета javax.persistence: @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded. Такие поля отражаются в метаданных автоматически. Поэтому @MetaProperty в основном применяется для определения неперсистентных атрибутов сущностей.

Параметры:

  • mandatory - (необязательный параметр, по умолчанию false) - может ли атрибут содержать null. При указании mandatory = true визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

Пример использования для поля:

@Transient
@MetaProperty
protected String token;

Пример использования для метода:

@MetaProperty
public String getLocValue() {
  if (!StringUtils.isBlank(messagesPack)) {
      return AppBeans.get(Messsages.class).getMessage(messagesPack, value);
  } else {
      return value;
  }
}
@OnDelete

Определяет политику обработки связи в случае мягкого удаления сущности, содержащей данный атрибут. См. Раздел 4.2.1.4, «Мягкое удаление»

Пример:

@OneToMany(mappedBy = "group")
@OnDelete(DeletePolicy.CASCADE)
private Set<Constraint> constraints;
@OnDeleteInverse

Определяет политику обработки связи в случае мягкого удаления сущности с обратной стороны ассоциации. См. Раздел 4.2.1.4, «Мягкое удаление»

Пример:

@ManyToOne
@JoinColumn(name = "DRIVER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
private Driver driver;
@Composition

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

Например, список пунктов в заказе (класс Order содержит коллекцию экземпляров Item):

@OneToMany(mappedBy = "order")
@Composition
protected List<Item> items;

Указание для связи аннотации @Composition позволяет организовать в экранах редактирования специальный режим коммита источников данных, при котором изменения экземпляров детализирующей сущности сохраняются в базе данных только при коммите основной сущности. Подробнее см. Раздел 5.8.3, «Редактирование композитных сущностей».

@LocalizedValue

Служит для описания способа получения локализованного значения некоторого изменяющегося атрибута, которое возвращает метод MessageTools.getLocValue().

Параметры:

  • messagePack - явное указание пакета, из которого будет взято локализованное сообщение, например, com.haulmont.cuba.core.entity

  • messagePackExpr - выражение в терминах пути к атрибуту, хранящему имя пакета, из которого будет взято локализованное сообщение, например proc.messagesPack. Путь начинается с атрибута текущей сущности.

Пример аннотации, означающей, что локализованное значение атрибута state будет взято из пакета, имя которого хранится в атрибуте messagesPack связанной сущности proc:

@Column(name = "STATE")
@LocalizedValue(messagePackExpr = "proc.messagesPack")
protected String state;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROC_ID")
protected Proc proc;
@IgnoreUserTimeZone

Для атрибутов типа timestamp с аннотацией @javax.persistence.Temporal.TIMESTAMP заставляет платформу игнорировать часовой пояс пользователя, если он задан для текущей сессии.

4.2.1.3. Атрибуты типа enum

В стандартном варианте использования JPA для атрибутов типа enum в базе данных хранится целое число, получаемое методом ordinal() этого перечисления. Такой подход может привести к следующим проблемам при эксплуатации и развитии системы:

  • при появлении в БД значения, не равного ни одному ordinal значению перечисления, экземпляр сущности нельзя загрузить вообще;

  • невозможно ввести новое значение между имеющимися, что актуально при использовании сортировки по значению перечисления (order by).

Чтобы решить эти проблемы, в подходе CUBA предлагается отвязать значение, хранимое в БД, от 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);

  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;
  }
}

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

Как видно из примеров, для атрибута grade в БД хранится значение типа Integer, задаваемое полем id перечисления CustomerGrade, а конкретно 10, 20 или 30. В то же время прикладной код и метаданные работают с самим типом CustomerGrade через методы доступа, которые и осуществляют конвертацию.

При наличии в поле БД значения, не соответствующего ни одному значению перечисления, метод getGrade() просто вернет null. Для ввода нового значения, например, HIGHER, между HIGH и PREMIUM, достаточно добавить это значение в перечисление с идентификатором 15, при этом сортировка по полю Customer.grade останется верной.

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

4.2.1.4. Мягкое удаление

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

Механизм мягкого удаления является "прозрачным" для прикладного программиста - достаточно убедиться, что класс сущности реализует интерфейс SoftDelete, и платформа сама нужным образом будет модифицировать запросы и операции с данными.

Режим мягкого удаления имеет следующие преимущества:

  • значительно снижается риск потери данных вследствие неверных действий пользователей

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

    Возьмем для примера модель данных Заказы - Покупатели. Допустим, на некоторого покупателя оформлено несколько заказов, однако нам нужно сделать его недоступным для дальнейшей работы. Традиционным "жестким" удалением сделать это невозможно, так как для удаления покупателя нам нужно либо удалить все его заказы, либо обнулить в этих заказах ссылки на него (т.е. потерять информацию). При мягком удалении покупателя он становится недоступным для поиска и изменения, однако при просмотре заказов пользователь видит на экране название покупателя, так как при загрузке связей признак удаления намеренно игнорируется.

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

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

4.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-запросу, а также удаленные элементы связанных сущностей в атрибутах-коллекциях. Однако связанные сущности в единичных атрибутах загружаются независимо от того, удален связанный экземпляр или нет.

4.2.1.4.2. Политика обработки связей

Платформа предоставляет средство обработки связей при удалении сущностей, во многом аналогичное правилам ON DELETE внешних ключей в базе данных. Это средство работает на уровне Middleware и использует аннотации @OnDelete, @OnDeleteInverse атрибутов сущности.

Аннотация @OnDelete обрабатывается при удалении той сущности, в которой она встретилась, а не той, на которую указывает аннотированный атрибут (в этом отличие от каскадных удалений на уровне БД).

Аннотация @OnDeleteInverse обрабатывается при удалении той сущности, на которую указывает аннотированный атрибут, (т.е. аналогично каскадному удалению на уровне внешних ключей в БД). Эта аннотация полезна при отсутствии в удаляемом объекте атрибута, который нужно проверять при удалении. При этом, как правило, в проверяемом объекте существует ссылка на удаляемый, на этот атрибут и устанавливается аннотация @OnDeleteInverse.

Значением аннотации может быть:

  • DeletePolicy.DENY - запретить удаление сущности, если аннотированный атрибут не null или не пустая коллекция

  • DeletePolicy.CASCADE - каскадно удалить аннотированный атрибут

  • DeletePolicy.UNLINK - разорвать связь с аннотированным атрибутом. Разрыв связи имеет смысл указывать только на ведущей стороне ассоциации - той, которая в классе сущности аннотирована @JoinColumn.

Примеры:

  1. Запрет удаления при наличии ссылки: при попытке удаления экземпляра Customer, на который ссылается хотя бы один Order, будет выброшено исключение DeletePolicyException.

    Order.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID")
    @OnDeleteInverse(DeletePolicy.DENY)
    protected Customer customer;

    Customer.java

    @OneToMany(mappedBy = "customer")
    protected List<Order> orders;
  2. Каскадное удаление элементов коллекции: при удалении экземпляра Role все экземпляры Permission также будут удалены.

    Role.java

    @OneToMany(mappedBy = "role")
    @OnDelete(DeletePolicy.CASCADE)
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    protected Role role;
  3. Разрыв связи с элементами коллекции: удаление экземпляра Role приведет к установке в null ссылок со стороны всех входивших в коллекцию экземпляров Permission.

    Role.java

    @OneToMany(mappedBy = "role")
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    @OnDeleteInverse(DeletePolicy.UNLINK)
    protected Role role;

Особенности реализации:

  1. Нужно быть осторожным при использовании @OnDeleteInverse с политиками CASCADE и UNLINK, так как при этом происходит извлечение из БД на сервер приложения всех экземпляров ссылающихся объектов, изменение и затем сохранение.

    Например, в случае ассоциации Customer - Job и большого количества работ для одного заказчика, если поставить на атрибут Job.customer политику @OnDeleteInverse(CASCADE), то при удалении экземпляра заказчика будет предпринята попытка извлечь и изменить все его работы. Это может привести к перегрузке сервера приложения и БД.

    С другой стороны, использование @OnDeleteInverse(DENY) безопасно, так как при этом производится только подсчет количества ссылающихся объектов, и если оно больше 0, выбрасывается исключение. Поэтому @OnDeleteInverse(DENY) для атрибута Job.customer вполне допустимо.

  2. Политика обработки связей реализуется с помощью Entity Listeners, то есть при сохранении данных в БД на уровне Middleware.

4.2.1.4.3. Ограничение уникальности на уровне БД

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

Реализуется данная логика путем, специфичным для используемого сервера базы данных:

  • Если сервер БД поддерживает частичные (partial) индексы (например, PostgreSQL), то ограничение уникальности можно создать следующим образом:

    create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
  • Если сервер БД не поддерживает частичные индексы (например, Microsoft SQL Server 2005), то в уникальный индекс можно включить поле DELETE_TS:

    create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)

4.2.2. Metadata Framework

Для эффективной работы с моделью данных в CUBA-приложениях используется фреймворк метаданных, который:

  • предоставляет удобный интерфейс для получения информации о сущностях, их атрибутах и отношениях между сущностями; а также для навигации по ссылкам

  • служит специализированной и более удобной в использовании альтернативой Java Reflection API

  • регламентирует допустимые типы данных и отношений между сущностями

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

4.2.2.1. Интерфейсы метаданных

Рассмотрим основные интерфейсы метаданных.

Рисунок 10. Интерфейсы фреймворка метаданных

Интерфейсы фреймворка метаданных

Session

Точка входа в фреймворк метаданных. Позволяет получать экземпляры MetaClass по имени и по соответствующему классу Java. Обратите внимание на различие методов getClass() и getClassNN() - первые могут возвращать null, вторые нет (NonNull).

Объект Session может быть получен через интерфейс инфраструктуры Metadata .

Пример:

@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
MetaModel

Редко используемый интерфейс, служит для группировки мета-классов.

Группировка осуществляется по имени корневого Java пакета проекта, указываемого в файле metadata.xml .

MetaClass

Интерфейс метаданных класса сущности. MetaClass всегда ассоциирован с классом Java, которого он представляет.

Основные методы:

  • getName() – имя сущности, по соглашению первой частью имени до знака $ является код пространства имен, например, sales$Customer

  • getProperties() – список мета-свойств (MetaProperty)

  • getProperty(), getPropertyNN() - получение мета-свойства по имени. Первый метод в случае отсутствия атрибута с указанным именем возвращает null, второй выбрасывает исключение.

    Пример:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupProperty = userClass.getPropertyNN("group");
  • getPropertyPath() - позволяет перемещаться по ссылкам. Данный метод принимает строковый параметр - путь из имен атрибутов, разделенных точкой. Возвращаемый объект MetaPropertyPath позволяет обратиться к искомому (последнему в пути) атрибуту вызовом getMetaProperty().

    Пример:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty();
    assert groupNameProp.getDomain().getName().equals("sec$Group");
  • getJavaClass() – класс сущности, которому соответствует данный MetaClass

  • getAnnotations() – коллекция мета-аннотаций

MetaProperty

Интерфейс метаданных атрибута сущности.

Основные методы:

  • getName() – имя свойства, соответствует имени атрибута сущности

  • getDomain() – мета-класс, которому принадлежит данное свойство

  • getType() – тип свойства:

    • простой тип: DATATYPE

    • перечисление: ENUM

    • ссылочный тип двух видов:

      • ASSOCIATION − простая ссылка на другую сущность. Например, отношение заказа и покупателя − ассоциация.

      • COMPOSITION − ссылка на сущность, которая не имеет самостоятельного значения без владеющей сущности. COMPOSITION можно считать "более тесным" отношением, чем ASSOCIATION. Например, отношение заказа и пункта этого заказа − COMPOSITION, т.к. пункт не может существовать без заказа, которому он принадлежит.

      Вид ссылочного атрибута ASSOCIATION или COMPOSITION влияет на режим редактирования сущности: в первом случае сохранение связанной сущности в базу данных происходит независимо, а во втором − связанная сущность сохраняется в БД только вместе с владеющей сущностью.

  • getRange() – интерфейс Range, детально описывающий тип данного атрибута

  • isMandatory() – признак обязательности атрибута. Используется, например, визуальными компонентами для сигнализации пользователю о необходимости ввода значения.

  • isReadOnly() – признак неизменности атрибута

  • getInverse() – для ссылочного атрибута возвращает мета-свойство с обратной стороны ассоциации, если таковое имеется

  • 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

4.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).

4.2.2.3. Datatype

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

Все экземпляры зарегистрированы в репозитории - классе Datatypes, который выполняет загрузку и инициализацию классов реализации Datatype следующим образом:

  • в корне CLASSPATH ищется файл datatypes.xml , и если он найден, репозиторий Datatypes инициализируется из него

  • в противном случае инициализация Datatypes производится из файла /com/haulmont/chile/core/datatypes/datatypes.xml

Экземпляр Datatype может быть получен двумя способами:

  • для атрибута сущности из соответствующего ему мета-свойства типа DATATYPE , вызовом getRange().asDatatype()

  • статическим методом Datatypes.get(), передавая в него имя реализации Datatype или класс Java, для которого он создан.

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

  • Если для поля или метода задана аннотация @MetaProperty с непустым значением datatype, то атрибуту сопоставляется экземпляр Datatype с данным именем.

    Например, при следующем объявлении атрибута сущности он получит нестандартный тип GeoCoordinateDatatype (см. пример ниже):

    @MetaProperty(datatype = GeoCoordinateDatatype.NAME)
    @Column(name = "LATITUDE")
    private Double latitude;
  • как правило, явное указание отсутствует, и атрибуту сопоставляется экземпляр Datatype, возвращаемый репозиторием Datatypes.get(Class), при передаче в него типа поля или метода.

    Например, в данном случае атрибут latitude получит стандартный тип DoubleDatatype, зарегистрированный в базовом /com/haulmont/chile/core/datatypes/datatypes.xml:

    @Column(name = "LATITUDE")
    private Double latitude;

Основные методы интерфейса Datatype:

  • getName() - возвращает уникальное имя данной реализации

  • format() - преобразовывает переданное значение в строку

  • parse() - преобразовывает строку в значение нужного типа

Datatype определяет два набора методов для форматирования/парсинга: с учетом локали и без учета локали. Преобразование с учетом локали используется повсеместно в пользовательском интерфейсе, преобразование без учета локали используется в системных механизмах, например, для сериализации в REST API.

Форматы для преобразований без учета локали задаются в вышеупомянутом файле datatypes.xml .

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

  • numberDecimalSeparator - задает символ разделителя целой и дробной части для числовых типов

  • numberGroupingSeparator - задает символ разделителя групп разрядов для числовых типов

  • integerFormat - формат для типов Integer и Long

  • doubleFormat - формат для типа Double

  • decimalFormat - формат для типа BigDecimal

  • dateTimeFormat - формат для типа java.util.Date

  • dateFormat - формат для типа java.sql.Date

  • timeFormat - формат для типа java.sql.Time

  • trueString - строка, соответствующая Boolean.TRUE

  • falseString - строка, соответствующая Boolean.FALSE

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

4.2.2.3.1. Пример форматирования даты в UI

Рассмотрим отображение атрибута Order.date в таблице браузера заказов.

order-browse.xml

<table id="ordersTable">
  ...
  <columns>
      <column id="date"/>
      ...

Атрибут date в классе Order определен с типом "дата":

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

Если текущий пользователь зарегистрирован c русской локалью, то из главного пакета локализованных сообщений клиентского уровня, из файла messages_ru.properties извлекается строка:

dateFormat=dd.MM.yyyy

Результат: дата "6 августа 2012 года" конвертируется в строку "06.08.2012" для отображения в ячейке таблицы.

4.2.2.3.2. Примеры форматирования дат и чисел в коде приложения
  • Пример форматирования даты

    @Inject
    protected UserSessionSource userSessionSource;
    ...
    Date date = ...;
    String dateStr = Datatypes.get(Date.class).format(date, userSessionSource.getLocale());
  • Пример форматирования числового значения с повышенной точностью (5 знаков после запятой) в блоке Web Client

    /com/sample/sales/web/messages_ru.properties

    coordinateFormat = #,##0.00000

    SomeClass.java

    @Inject
    protected Messages messages;
    @Inject
    protected UserSessionSource userSessionSource;
    ...
    String coordinateFormat = messages.getMainMessage("coordinateFormat");
    FormatStrings formatStrings = Datatypes.getFormatStrings(userSessionSource.getLocale());
    NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols());
    
    String formattedValue = format.format(value);
4.2.2.3.3. Пример специализированного Datatype

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

Создаем класс в модуле global:

public class GeoCoordinateDatatype extends DoubleDatatype {

  public static final String NAME = "geocoordinate";

  // формат общий для всех локалей, отличаться могут только символы десятичной точки
  public static final String FORMAT = "#0.000000";

  public GeoCoordinateDatatype(Element element) {
      super(element);
  }

  @Override
  public String getName() {
      return NAME;
  }

  @Override
  public String format(Double value, Locale locale) {
      if (value == null)
          return "";
      FormatStrings formatStrings = Datatypes.getFormatStrings(locale);
      if (formatStrings == null)
          return format(value); // FormatStrings для локали не определены, форматируем по данным datatypes.xml

      NumberFormat format = new DecimalFormat(FORMAT, formatStrings.getFormatSymbols());
      return format.format(value);
  }

  @Override
  public Double parse(String value, Locale locale) throws ParseException {
      if (StringUtils.isBlank(value))
          return null;
      FormatStrings formatStrings = Datatypes.getFormatStrings(locale);
      if (formatStrings == null)
          return parse(value); // FormatStrings для локали не определены, парсим по данным datatypes.xml

      NumberFormat format = new DecimalFormat(FORMAT, formatStrings.getFormatSymbols());
      return parse(value, format).doubleValue();
  }
}

Создаем файл datatypes.xml в корне каталога src модуля global проекта приложения и копируем в него все из файла /com/haulmont/chile/core/datatypes/datatypes.xml, расположенного в модуле global базового проекта cuba. Затем добавляем в него регистрацию нового типа:

<datatypes>

  <datatype class="com.sample.sales.entity.GeoCoordinateDatatype"
            format="#0.000000" decimalSeparator="." groupingSeparator=""/>
...

Указываем новый тип данных для требуемых атрибутов:

@MetaProperty(datatype = GeoCoordinateDatatype.NAME)
@Column(name = "LATITUDE")
private Double latitude;

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

4.2.2.4. Мета-аннотации

Мета-аннотации сущностей - набор пар ключ/значение, содержащих дополнительную информацию о сущностях.

Обращение к мета-аннотациям производится с помощью метода мета-класса getAnnotations().

Источниками мета-аннотаций сущности являются:

  • Аннотации @OnDelete, @OnDeleteInverse, @Extends. При этом в мета-аннотациях создаются служебные объекты связей между сущностями.

  • Аннотации @NamePattern, @SystemLevel, @EnableRestore, @TrackEditScreenHistory. При этом создаются мета-аннотации с ключами, соответствующими полному имени класса Java аннотации.

  • Опционально: в прикладном проекте могут быть определены свои аннотации для сущностей, и в переопределенном методе MetadataImpl.initMetaAnnotations() отображены в соответствующие мета-аннотации.

  • Опционально: в файлах metadata.xml также могут быть определены мета-аннотации сущностей. Если мета-аннотация в XML имеет то же имя, что и мета-аннотация, созданная по Java аннотации класса сущности, первая переопределит значение второй.

    Пример определения мета-аннотаций в metadata.xml :

    <annotations>
      <entity class="com.haulmont.cuba.security.entity.User">
          <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory" value="false"/>
          <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore" value="true"/>
      </entity>
    </annotations>

4.2.3. Представления

При извлечении сущностей из базы данных обычно встает вопрос - как обеспечить загрузку связанных сущностей на нужную глубину?

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

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

Другой похожей проблемой является ограничение набора локальных атрибутов сущностей загружаемого графа: например, некоторая сущность имеет 50 атрибутов, в том числе BLOB, а в экране отображается только 10 атрибутов. Зачем загружать из БД, затем сериализовать и передавать клиенту 40 атрибутов, которые ему в данный момент не нужны?

Механизм представлений (views) решает эти проблемы, обеспечивая извлечение из базы данных и передачу клиенту графов сущностей, ограниченных в глубину и по атрибутам. Представление является описателем графа объектов, который требуется в некотором экране UI или другом процессе обработки данных.

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

  • Все связи в модели данных объявляются с признаком загрузки по требованию (fetch = FetchType.LAZY, см. Раздел 4.2.1.2, «Аннотации сущностей»).

  • В процессе загрузки данных через DataManager клиентский код помимо JPQL запроса указывает нужное представление.

  • На основе представления формируется так называемый Fetch Plan - особенность лежащего в основе слоя ORM фреймворка Apache OpenJPA. Fetch Plan влияет на формирование SQL запроса к базе данных: как на список возвращаемых полей, так и на соединения с другими таблицами, содержащими связанные сущности.

  • В представлении некоторые ссылочные атрибуты могут быть объявлены как lazy (см. ниже). Lazy-атрибуты не включаются в Fetch Plan, а загружаются отдельными SQL запросами (иногда это полезно для упрощения основного SQL запроса). Для этого механизм обработки представлений просто обращается к соответствующим методам чтения атрибутов.

  • В результате на момент завершения транзакции, загружающей данные, в памяти Middleware содержится граф объектов, заданный JPQL запросом и представлением.

Рисунок 11. Классы представления

Классы представления

Представление определяется экземпляром класса View, в котором:

  • entityClass - класс сущности, для которого определено представление. Другими словами, "корень" дерева загружаемых сущностей.

  • name - имя представления. Должно быть либо null, либо уникальным в пределах данной сущности.

  • properties - коллекция экземпляров класса ViewProperty, соответствующих загружаемым атрибутам сущности.

  • includeSystemProperties - признак включения системных атрибутов (входящих в состав базовых интерфейсов персистентных сущностей BaseEntity и Updatable). Системные атрибуты не перечисляются в properties явно, а учитываются механизмом обработки представлений в зависимости от того, какие интерфейсы реализует данная сущность.

Класс ViewProperty имеет следующие свойства:

  • name - имя атрибута сущности

  • view - для ссылочных атрибутов задает представление, с которым необходимо загружать связанную сущность

  • lazy - для ссылочных атрибутов признак того, что данный атрибут нужно не включать в Fetch Plan, а загружать отдельным SQL запросом, инициированным обращением к атрибуту. Следует иметь в виду, что при использовании DataManager и источников данных атрибут в любом случае будет загружен, данный признак влияет только на способ загрузки. Если же представление с lazy атрибутами используется на уровне ORM, то после загрузки экземпляров их обязательно нужно передать в метод EntityManager.fetch() до окончания транзакции, иначе lazy атрибуты загружены не будут.

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

  • id - идентификатор сущности

  • version - для оптимистично блокируемых сущностей, реализующих Versioned

  • deleteTs, deletedBy - для сущностей, реализующих SoftDelete

Незагруженные атрибуты имеют значение null. По умолчанию попытка установки значения незагруженного атрибута (вызов setter) для Detached сущности вызывает исключение. Это поведение можно изменить с помощью свойства приложения cuba.allowSetNotLoadedAttributes . Если данное свойство установлено в true, то вызов setter не приведет к исключению, но значение все равно сохранено не будет.

Следует иметь в виду, что незагруженные ссылочные атрибуты Detached сущности, соответствующие внешним ключам (т.е. ManyToOne, OneToOne), можно установить в новое ненулевое значение в любом случае, и изменения будут сохранены при последующем merge().

4.2.3.1. Создание представлений

Представление может быть создано двумя путями:

  • программно - созданием экземпляра View, например:

    View view = new View(Order.class)
          .addProperty("date")
          .addProperty("amount")
          .addProperty("customer", new View(Customer.class)
              .addProperty("name")
          );

    Как правило, таким способом создаются представления, используемые только в каком-то одном месте бизнес-логики.

  • декларативно - путем создания описателя на XML и его развертывания в репозитории представлений ViewRepository. При развертывании на основе XML-описателя создаются и кэшируются экземпляры View. В дальнейшем в любом месте кода приложения требуемое представление можно получить вызовом репозитория с указанием класса сущности и имени представления.

Рассмотрим подробнее декларативный способ создания и работы с представлениями.

ViewRepository является бином Spring, доступным для всех блоков приложения. Ссылка на ViewRepository может быть также получена через интерфейс инфраструктуры Metadata . Для получения экземпляра View, содержащегося в репозитории, используются методы getView(). Для развертывания XML-описателей представлений в репозитории используются методы deployViews() базовой реализации AbstractViewRepository.

В репозитории для каждой сущности по умолчанию доступны два представления с именами _local и _minimal:

  • _local включает в себя все локальные атрибуты сущности

  • _minimal включает в себя атрибуты, входящие в имя экземпляра сущности, и задаваемые аннотацией @NamePattern . Если аннотация @NamePattern для сущности не указана, данное представление не включает никаких атрибутов.

Подробная структура XML-описателей изложена в Раздел A.11, «views.xml»

Пример описателя представления для сущности Заказ, которое должно обеспечить загрузку всех локальных атрибутов, ассоциированного Покупателя и коллекции Пунктов заказа:

<view class="com.sample.sales.entity.Order"
    name="orderWithCustomer"
    extends="_local">
  <property name="customer" view="_minimal"/>
  <property name="items" view="itemsInOrder"/>
</view>

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

  • В модуле global в корне src создать файл views.xml и поместить в него все описатели представлений, которые должны быть доступны глобально, т.е. на всех уровнях приложения.

  • Зарегистрировать данный файл в свойстве cuba.viewsConfig блока Middleware и используемых клиентских блоков, т.е. в файле app.properties модуля core, в файле web-app.properties модуля web и так далее. Это обеспечит автоматическое развертывание представлений на старте приложения в репозитории Middleware и клиентских блоков (см. метод AbstractViewRepository.init()).

  • Если существуют представления, которые необходимы только какому-то одному клиентскому блоку приложения, то можно определить их в аналогичном файле данного блока, например, web-views.xml, и добавить этот файл в свойство cuba.viewsConfig этого блока, т.е. в данном случае в файл web-app.properties.

Если на момент развертывания некоторого представления в репозитории уже есть представление для этого же класса сущности и с таким же именем, то новое будет проигнорировано. Для того чтобы представление заменило имеющееся в репозитории и гарантированно было развернуто, в XML-описателе должен быть явно указан атрибут overwrite = "true".

Рекомендуется давать представлениям "описательные" имена. Например, не "browse", а "customerBrowse". Это упрощает поиск XML-описателей представлений по имени в процессе разработки приложения.

4.2.4. Управляемые бины

Управляемые бины (Managed Beans) − это программные компоненты, предназначенные для реализации бизнес-логики приложения. Термин "управляемые" в данном случае означает, что созданием экземпляров и установкой связей между такими компонентами управляет контейнер, который является основной частью фреймворка Spring.

Managed Bean представляет собой singleton, то есть в некотором блоке приложения существует только один экземпляр данного класса. Поэтому, если бин содержит изменяемые данные в полях (другими словами, имеет состояние), то обращение к таким данным необходимо синхронизировать.

4.2.4.1. Создание бина

Для создания управляемого бина достаточно добавить классу Java аннотацию @javax.annotation.ManagedBean. Например:

package com.sample.sales.core;

import com.sample.sales.entity.Order;
import javax.annotation.ManagedBean;

@ManagedBean(OrderWorker.NAME)
public class OrderWorker {
  public static final String NAME = "sales_OrderWorker";

  public void calculateTotals(Order order) {
  }
}

Рекомендуется присваивать бину уникальное имя вида {имя_проекта}_{имя_класса}, и определять его в константе NAME.

Класс управляемого бина должен находиться внутри дерева пакетов с корнем, заданным в элементе context:component-scan файла spring.xml . В нашем случае файл spring.xml содержит элемент:

<context:component-scan base-package="com.sample.sales"/>

что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.

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

package com.sample.sales.core;

import com.sample.sales.entity.Order;

public interface OrderWorker {
  String NAME = "sales_OrderWorker";

  void calculateTotals(Order order);
}
package com.sample.sales.core;

import com.sample.sales.entity.Order;
import javax.annotation.ManagedBean;

@ManagedBean(OrderWorker.NAME)
public class OrderWorkerBean implements OrderWorker {
  @Override
  public void calculateTotals(Order order) {
  }
}

Управляемые бины можно создавать на любом уровне, так как контейнер Spring Framework используется во всех стандартных блоках приложения.

4.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.Transactional;
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, который и содержит основную бизнес-логику.

4.2.5. JMX-бины

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

Рисунок 12.


Как видно из диаграммы, JMX-бин состоит из интерфейса и класса реализации. Класс должен представлять собой управляемый бин, то есть иметь аннотацию @ManagedBean и уникальное имя. Интерфейс JMX-бина специальным образом регистрируется в spring.xml для создания в текущей JVM собственно JMX-интерфейса.

Вызовы всех методов интерфейса JMX-бина перехватываются с помощью Spring AOP классом−интерцептором MBeanInterceptor, который обеспечивает установку правильного ClassLoader в контексте потока выполнения, и журналирование необработанных исключений.

Интерфейс JMX-бина обязательно должен иметь имя вида {имя_класса}MBean.

С JMX-интерфейсом можно работать из внешних инструментов, таких как jconsole или jvisualvm. Кроме того, в состав блока Web Client платформы входит JMX консоль, предоставляющая базовые средства просмотра состояния и вызова методов JMX-бинов.

4.2.5.1. Создание 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")
      @ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")})
      String calculateTotals(String orderId);
    }

    Интерфейс и его методы могут содержать аннотации для задания описания JMX-бина и его операций. Это описание будет отображаться во всех инструментах, работающих с данным JMX-интерфейсом, тем самым помогая администратору системы.

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

  • Класс 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 javax.annotation.ManagedBean;
    import javax.inject.Inject;
    import java.util.UUID;
    
    @ManagedBean("sales_OrdersMBean")
    public class Orders implements OrdersMBean {
    
      @Inject
      protected OrderWorker orderWorker;
    
      @Inject
      protected Persistence persistence;
    
      @Authenticated
      @Override
      public String calculateTotals(final String orderId) {
          try {
              persistence.createTransaction().execute(new Transaction.Runnable() {
                  @Override
                  public void run(EntityManager em) {
                      Order entity = em.find(Order.class, UUID.fromString(orderId));
                      orderWorker.calculateTotals(entity);
                  }
              });
              return "Done";
          } catch (Throwable e) {
              return ExceptionUtils.getStackTrace(e);
          }
      }
    }

    Аннотация @ManagedBean определяет, что данный класс является управляемым бином с именем sales_OrdersMBean. Имя указано напрямую в аннотации, а не в константе, так как доступ к JMX-бину из кода Java не требуется.

    Рассмотрим реализацию метода calculateTotals().

    • Метод имеет аннотацию @Authenticated, т.е. при входе в метод и при отсутствии в потоке выполнения пользовательской сессии выполняется системная аутентификация.

    • Тело метода обернуто в блок try/catch, так что метод в случае успешного выполнения возвращает строку "Done", а в случае ошибки - stacktrace исключения в виде строки.

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

    • Логика метода заключается в том, что он стартует транзакцию, загружает экземпляр сущности Order по идентификатору, и передает управление бину OrderWorker для обработки.

  • Регистрация JMX-бина в spring.xml:

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

Все JMX-бины проекта объявляются в одном экземпляре MBeanExporter в элементах map/entry свойства beans. Ключом элемента здесь является JMX ObjectName, значением - имя бина, заданное в аннотации @ManagedBean. ObjectName начинается с имени веб-приложения, так как в одном экземпляре Tomcat (т.е. в одной JVM) может быть развернуто несколько веб-приложений, экспортирующих одинаковые JMX-интерфейсы.

4.2.5.2. JMX-бины платформы

В данном разделе описаны некоторые имеющиеся в платформе JMX-бины.

4.2.5.2.1. CachingFacadeMBean

CachingFacadeMBean предоставляет методы очистки различных кэшей в блоках Middleware и Web Client.

JMX ObjectName: app-core.cuba:type=CachingFacade и app.cuba:type=CachingFacade

4.2.5.2.2. ConfigStorageMBean

ConfigStorageMBean позволяет просматривать и задавать значения свойствам приложения в блоках Middleware, Web Client и Web Portal.

Данный интерфейс имеет отдельные наборы методов для работы с параметрами конфигурации и развертывания (*AppProperties) и с параметрами времени выполнения (*DbProperties), что обусловлено различием механизмов хранения этих категорий свойств.

Существуют следующие ограничения в использовании интерфейса ConfigStorageMBean:

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

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

JMX ObjectName: app-core.cuba:type=ConfigStorage, app.cuba:type=ConfigStorage, app-portal.cuba:type=ConfigStorage

4.2.5.2.3. EmailerMBean

EmailerMBean позволяет просмотреть текущие значения параметров отсылки email, а также отправить тестовое сообщение.

JMX ObjectName: app-core.cuba:type=Emailer

4.2.5.2.4. PersistenceManagerMBean

PersistenceManagerMBean предоставляет следующие возможности:

  • управление механизмом статистики сущностей

  • отображение новых скриптов обновления БД методом findUpdateDatabaseScripts() и запуск обновления методом updateDatabase()

  • запуск произвольных JPQL запросов в контексте Middleware методами jpqlLoadList(), jpqlExecuteUpdate()

JMX ObjectName: app-core.cuba:type=PersistenceManager

4.2.5.2.5. ScriptingManagerMBean

ScriptingManagerMBean является JMX-фасадом для интерфейса инфраструктуры Scripting .

JMX ObjectName: app-core.cuba:type=ScriptingManager

JMX атрибуты:

JMX операции:

  • runGroovyScript() - выполнить скрипт Groovy в контексте Middleware и вернуть результат. В скрипт передаются следующие переменные:

    Для отображения в 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()
    } finally {
      tx.end()
    }
4.2.5.2.6. ServerInfoMBean

ServerInfoMBean предоставляет общую информацию о данном блоке Middleware: номер и дату сборки, идентификатор сервера.

JMX ObjectName: app-core.cuba:type=ServerInfo

4.2.6. Интерфейсы инфраструктуры

Интерфейсы инфраструктуры обеспечивают доступ к часто используемой функциональности платформы. Большинство из этих интерфейсов расположены в модуле global и могут быть использованы как на среднем слое, так и в блоках клиентского уровня, но некоторые (например, Persistence ) доступны только коду среднего слоя.

Интерфейсы инфраструктуры реализуются бинами Spring Framework, поэтому они могут быть инжектированы в любые другие управляемые компоненты (Managed Beans, сервисы среднего слоя, контроллеры экранов универсального пользовательского интерфейса.

Кроме того, как и любые другие бины, интерфейсы инфраструктуры могут быть получены с помощью статических методов класса AppBeans, и использоваться в неуправляемых компонентах (POJO, вспомогательных классах и пр.).

4.2.6.1. Configuration

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

Пример:

String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();

4.2.6.2. Messages

Интерфейс Messages обеспечивает получение локализованных строк сообщений.

Рассмотрим методы интерфейса подробнее.

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

    Примеры:

    @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);
  • formatMessage() - находит локализованное сообщение по ключу, имени пакета сообщений и требуемой локали, и использует его для форматирования переданных параметров. Формат задается по правилам метода String.format(). Существует несколько модификаций данного метода в зависимости от набора параметров. Если локаль не указана в параметре метода, используется локаль текущего пользователя.

    Пример:

    String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
  • getMainMessage() - возвращает локализованное сообщение из главного пакета данного блока приложения.

    Пример:

    protected Messages messages = AppBeans.get(Messages.class);
    ...
    messages.getMainMessage("actions.Ok");
  • getMainMessagePack() - возвращает имя главного пакета сообщений данного блока приложения.

    Пример:

    String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
  • getTools() - возвращает экземпляр интерфейса MessageTools (см. ниже).

4.2.6.2.1. MessageTools

ManagedBean, содержащий вспомогательные методы работы с локализованными сообщениями. Интерфейс MessageTools можно получить либо методом Messages.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.

Методы MessageTools:

  • loadString() - возвращает локализованное сообщение, заданное ссылкой вида msg://{messagePack}/{key}.

    Составные части ссылки:

    • msg:// - обязательный префикс.

    • {messagePack} - необязательное имя пакета сообщения. Если не указано, предполагается, что имя пакета передается в loadString() отдельным параметром.

    • {key} - ключ сообщения в пакете.

    Примеры ссылок на сообщения:

    msg://someMessage
    msg://com.abc.sales.web.customer/someMessage
  • getEntityCaption() - возвращает локализованное название сущности.

  • getPropertyCaption() - возвращает локализованное название атрибута сущности.

  • hasPropertyCaption() - определяет, задано ли для атрибута сущности локализованное название.

  • getLocValue() - возвращает локализованное значение атрибута сущности, основываясь на определении аннотации @LocalizedValue .

  • getMessageRef() - формирует для мета-свойства ссылку на сообщение, по которой можно получить локализованное название атрибута сущности.

  • getDefaultLocale() - возвращает локаль приложения по умолчанию, то есть указанную первой в списке свойства cuba.availableLocales .

  • useLocaleLanguageOnly() - возвращает true, если в списке поддерживаемых приложением локалей, заданном свойством cuba.availableLocales , для всех локалей определен только язык, а country и variant не указаны. Этим методом пользуются механизмы платформы, которым необходимо найти наиболее подходящую локаль из списка поддерживаемых на основе локали, полученной из внешних источников, таких как операционная система или HTTP запрос.

  • trimLocale() - удаляет из переданной локали все кроме языка, если метод useLocaleLanguageOnly() возвращает true.

Для расширения набора вспомогательных методов в конкретном приложении бин MessageTools можно переопределить. Примеры работы с расширенным интерфейсом:

MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();

4.2.6.3. Metadata

Интерфейс Metadata обеспечивает доступ к сессии метаданных и репозиторию представлений.

Методы интерфейса:

4.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 можно переопределить. Примеры работы с расширенным интерфейсом:

MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();

4.2.6.4. Resources

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

  1. если указанное местонахождение представляет собой URL, ресурс загружается из этого URL;

  2. если указанное местонахождение начинается с префикса classpath:, ресурс загружается из classpath;

  3. если не URL и не начинается с classpath:, то:

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

    2. если ресурс не найден на предыдущих этапах, он загружается из classpath.

На практике явное указание URL или префикса classpath: используется редко, т.е. обычно ресурсы загружаются либо из конфигурационного каталога, либо из classpath. Ресурс в конфигурационном каталоге замещает одноименный ресурс в classpath.

Методы Resources:

  • getResourceAsStream() - возвращает InputStream для указанного ресурса, либо null, если ресурс не найден. Поток должен быть закрыт после использования, например:

    @Inject
    protected Resources resources;
    ...
    InputStream stream = null;
    try {
      stream = resources.getResourceAsStream(resourceLocation);
      ...
    } finally {
      IOUtils.closeQuietly(stream);
    }

    Возможно использование "try with resources":

    try (InputStream stream = resources.getResourceAsStream(resourceLocation)) {
      ...
    }
  • getResourceAsString() - возвращает указанный ресурс в виде строки, либо null, если ресурс не найден

4.2.6.5. Scripting

Интерфейс Scripting позволяет динамически (т.е. во время работы приложения) компилировать и загружать классы Java и Groovy, а также выполнять скрипты и выражения на Groovy.

Методы Scripting:

  • evaluateGroovy() - выполняет выражение на Groovy и возвращает его результат.

    Свойство приложения cuba.groovyEvaluatorImport позволяет определить общий набор импортируемых классов, подставляемых в каждое выполняемое выражение. По умолчанию все стандартные блоки приложения импортируют класс PersistenceHelper .

    Скомпилированные выражения кэшируются, что значительно ускоряет повторное выполнение.

    Пример:

    @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);
  • runGroovyScript() - выполняет скрипт Groovy и возвращает его результат.

    Скрипт должен быть расположен либо в конфигурационном каталоге приложения, либо в classpath (текущая реализация Scripting поддерживает ресурсы classpath только внутри JAR-файлов). Скрипт в конфигурационном каталоге замещает одноименный скрипт в classpath.

    Путь к скрипту указывается с разделителями /, в начале пути символ / не требуется.

    Пример:

    @Inject
    protected Scripting scripting;
    ...
    Binding binding = new Binding();
    binding.setVariable("itemId", itemId);
    BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
  • loadClass() - загружает Java или Groovy класс, используя следующую последовательность действий:

    1. Если класс уже загружен, возвращает его.

    2. Ищет исходный текст Groovy (файл *.groovy) в каталоге конфигурации. Если найден, компилирует его, загружает и возвращает класс.

    3. Ищет исходный текст Java (файл *.java) в каталоге конфигурации. Если найден, компилирует его, загружает и возвращает класс.

    4. Ищет скомпилированный класс в classpath, если найден - загружает и возвращает его.

    5. Если ничего не найдено, возвращает null.

    Файлы исходных текстов Java и Groovy в каталоге конфигурации можно изменять во время работы приложения. При следующем вызове loadClass() соответствующий класс будет перекомпилирован и возвращен новый, однако существуют следующие ограничения:

    • нельзя изменять тип исходного текста с Groovy на Java

    • если существовал исходный текст Groovy, и был однажды скомпилирован, то удаление файла исходного текста не приведет к загрузке другого класса из classpath - будет по-прежнему возвращаться класс, скомпилированный из удаленного исходника.

    Пример:

    @Inject
    protected Scripting scripting;
    ...
    Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
  • getClassLoader() - возвращает ClassLoader, способный работать по правилам, описанным выше для метода loadClass().

Кэш скомпилированных классов можно очистить во время выполнения с помощью JMX-бина CachingFacadeMBean.

См. также Раздел 4.2.5.2.5, «ScriptingManagerMBean»

4.2.6.6. Security

Обеспечивает авторизацию - проверку прав пользователя на различные объекты системы. Перед вызовом соответствующих методов UserSession выполняется поиск исходного мета-класса сущности, что является важным при наличии расширений. Кроме методов, дублирующих методы UserSession, данный интерфейс имеет методы isEntityAttrReadPermitted() и isEntityAttrUpdatePermitted(), предназначенные для определения доступности пути к атрибуту с учетом доступности атрибутов и сущностей, входящих в этот путь.

Подробнее см. Раздел 4.2.10, «Аутентификация пользователей»

4.2.6.7. TimeSource

Обеспечивает получение текущего времени. Применение new Date() и т.п. в прикладном коде не рекомендуется.

Примеры:

@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();

4.2.6.8. UserSessionSource

Обеспечивает получение объекта сессии текущего пользователя. Подробнее см. Раздел 4.2.10, «Аутентификация пользователей»

4.2.6.9. UuidSource

Обеспечивает получение значений UUID, в том числе для идентификаторов сущностей. Применение UUID.randomUUID() в прикладном коде не рекомендуется.

Для вызова из статического контекста можно использовать класс UuidProvider, который имеет также дополнительный метод fromString(), работающий быстрее, чем стандартный метод UUID.fromString().

4.2.6.10. DataManager

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

DataManager всегда стартует новую транзакцию и по завершении работы выполняет коммит, таким образом возвращая сущности в detached состоянии.

Методы DataManager:

  • load(), loadList() - загружает граф сущностей в соответствии с параметрами переданного объекта LoadContext.

    В LoadContext обязательно должен быть передан либо JPQL-запрос, либо идентификатор сущности. Если передано и то и другое, используется запрос, а идентификатор игнорируется.

    Правила создания запросов аналогичны описанным в Раздел 4.4.4.4, «Выполнение JPQL запросов». Отличием является то, что в запросе LoadContext могут быть использованы только именованные параметры, позиционные не поддерживаются.

    Методы load() и loadList() проверяют наличие у пользователя права EntityOp.READ на загружаемую сущность. Кроме того, при извлечении сущностей из БД накладываются ограничения групп доступа. Для отмены действия ограничений в текущем запросе можно передать в LoadContext атрибут useSecurityConstraints = false.

    Примеры загрузки сущностей в контроллере экрана:

    @Inject
    private DataManager dataManager;
    
    private Book loadBookById(UUID bookId) {
        LoadContext loadContext = new LoadContext(Book.class)
                .setId(bookId).setView("book.edit");
        return dataManager.load(loadContext);
    }
    
    private List<BookPublication> loadBookPublications(UUID bookId) {
        LoadContext loadContext = new LoadContext(BookPublication.class)
                .setView("bookPublication.full");
        loadContext.setQueryString("select p from library$BookPublication p where p.book.id = :bookId")
                .setParameter("bookId", bookId);
        return dataManager.loadList(loadContext);
    }

  • commit() - сохраняет в базе данных набор сущностей, переданный в объекте CommitContext. Отдельно указываются коллекции сущностей, которые нужно сохранить, и которые нужно удалить.

    Метод возвращает набор экземпляров сущностей, возвращенных из метода EntityManager.merge(), то есть по сути свежие экземпляры, только что обновленные в БД. Дальнейшая работа должна производиться именно с этими возвращенными экземплярами, чтобы предотвратить потерю данных или исключения оптимистичной блокировки. Для того, чтобы обеспечить наличие нужных атрибутов у возвращенных сущностей, с помощью мэп CommitContext.getViews() можно указать представление для каждого сохраняемого экземпляра.

    Метод commit() проверяет наличие у пользователя права EntityOp.UPDATE на изменяемые сущности, и EntityOp.DELETE на удаляемые.

    Примеры сохранения коллекций сущностей:

    @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.getCommitInstances().add(bookInstance);
            commitContext.getViews().put(bookInstance, view);
        }
        return dataManager.commit(commitContext);
    }

  • reload() - удобные методы для перезагрузки экземпляра сущности с требуемым представлением. Делегируют выполнение методу load().

  • remove() - удаляет экземпляр сущности из базы данных. Делегируют выполнение методу commit().

В процессе загрузки данных DataManager может реализовывать дополнительную функциональность, описанную ниже.

4.2.6.10.1. Запросы с distinct

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

  • при объединении с коллекцией на уровне извлечения из базы данных возникает набор с дубликатами строк

  • на клиентском уровне в источнике данных дубликаты исчезают, т.к. попадают в мэп (java.util.Map)

  • при постраничном отображении на одной странице оказывается меньшее количество строк, чем запрошено, общее количество строк наоборот завышено.

Таким образом, рекомендуется в JPQL запросы браузеров включать предложение distinct, которое гарантирует отсутствие дубликатов записей при выборке из базы данных. Однако в некоторых серверах БД (в частности PostgreSQL) при большом количестве извлекаемых записей (более 10000) SQL запрос с distinct выполняется недопустимо долго.

Для решения этой проблемы в платформе реализована возможность корректной работы без distinct на уровне SQL. Данный механизм включается свойством приложения cuba.inMemoryDistinct, при активации которого выполняется следующее:

  • В JPQL запросе должен по-прежнему присутствовать select distinct

  • В DataManager из JPQL запроса перед отправкой в ORM distinct вырезается

  • После загрузки страницы данных на Middleware удаляются дубликаты и выполняются дополнительные запросы к БД для получения нужного количества строк, которые затем и возвращаются клиенту.

4.2.6.10.2. Последовательная выборка

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

Данный механизм работает следующим образом:

  • При получении LoadContext с установленными атрибутами prevQueries и queryKey DataManager выполняет выборку по предыдущему запросу и сохраняет идентификаторы полученных сущностей в таблице SYS_QUERY_RESULT (соответствующей сущности sys$QueryResult), разделяя наборы записей по идентификаторам пользовательских сессий и ключу сеанса выборки queryKey.

  • Текущий запрос модифицируется для объединения с результатами предыдущего, так что в итоге возвращает данные, соответствующие условиям обоих запросов, объединенных по "И".

  • Далее процесс может повторяться, при этом уменьшающийся набор предыдущих результатов удаляется из таблицы SYS_QUERY_RESULT и заполняется заново.

Таблицу SYS_QUERY_RESULT необходимо периодически чистить от ненужных результатов запросов, оставленных завершенными пользовательскими сессиями. Для этого предназначен метод deleteForInactiveSessions бина QueryResultsManagerAPI. В прикладном проекте с включенным параметром cuba.allowQueryFromSelected необходимо вызывать этот метод из назначенных заданий, например:

<task:scheduled-tasks scheduler="scheduler">
      <task:scheduled ref="cuba_QueryResultsManager" method="deleteForInactiveSessions" fixed-rate="600000"/>
  </task:scheduled-tasks>

4.2.7. AppContext

AppContext - системный класс, в статических полях которого хранятся ссылки на некоторые общие для любого блока приложения компоненты:

  • ApplicationContext фреймворка Spring

  • Набор свойств приложения, загруженных из файлов app.properties

  • ThreadLocal переменная, хранящая экземпляры SecurityContext

  • Коллекция слушателей жизненного цикла приложения (AppContext.Listener)

AppContext инициализируется на запуске приложения классами-загрузчиками, специфичными для типа блока приложения:

  • загрузчик Middleware - AppContextLoader

  • загрузчик Web Client - WebAppContextLoader

  • загрузчик Web Portal - PortalAppContextLoader

  • загрузчик Desktop Client - DesktopAppContextLoader

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

  • Регистрации слушателей, срабатывающих после полной инициализации и перед закрытием приложения, например:

    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");
      }
    });

    В момент вызова applicationStarted():

    • Полностью инициализированы все бины, в том числе выполнены их методы @PostConstruct.

    • Можно использовать статические методы получения бинов AppBeans.get().

    • Метод AppContext.isStarted() возвращает true.

    • Метод AppContext.isReady() возвращает false.

    • В блоке Middleware: если свойство приложения cuba.automaticDatabaseUpdate включено, все скрипты обновления БД успешно выполнены.

    В момент вызова applicationStopped():

    • Все бины работоспособны и доступны через статические методы AppBeans.get().

    • Метод AppContext.isStarted() возвращает false.

    • Метод AppContext.isReady() возвращает false.

    Практический пример использования AppContext.Listener см. в Раздел 5.8.4, «Выполнение кода на старте приложения».

  • Получения значений свойств приложения, хранимых в файлах app.properties, если они недоступны через конфигурационные интерфейсы.

  • Передачи SecurityContext в новые потоки выполнения, см. Раздел 4.2.10, «Аутентификация пользователей».

Для получения ссылок на Spring-бины используйте инжекцию или статические методы класса AppBeans.

Использование AppContext.getApplicationContext().getBean() не рекомендуется.

4.2.8. Свойства приложения

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

По назначению свойства приложения можно классифицировать следующим образом:

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

    Например: cuba.springContextConfig, cuba.web.useLightHeader.

  • Параметры развертывания - различные URL для соединения блоков приложения, тип используемой БД, настройки подсистемы безопасности и т.д.

    Например: cuba.connectionUrlList, cuba.dbmsType, cuba.userSessionExpirationTimeoutSec .

  • Параметры времени выполнения - активность аудита, параметры отсылки email и т.д.

    Например: cuba.security.EntityLog.enabled, cuba.email.smtpHost.

Как правило, некоторое свойство принадлежит только одному или нескольким блокам приложения. Например, cuba.persistenceConfig имеет смысл только для Middleware, cuba.web.useLightHeader − только для Web Client, а cuba.springContextConfig − для всех блоков.

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

Принадлежность можно выяснить следующими способами:

4.2.8.1. Доступ к свойствам

Основной способ доступа к свойствам приложения из прикладного кода − использование механизма конфигурационных интерфейсов. Кроме того, все параметры конфигурации и развертывания доступны через методы класса AppContext .

Некоторые блоки приложения определяют JMX-интерфейсы для доступа к свойствам приложения. В частности, в блоках Middleware, Web Client и Web Portal имеется JMX-интерфейс ConfigStorageMBean , позволяющий получить и задать значение любого свойства во время работы приложения.

4.2.8.2. Хранение свойств в файлах

Свойства, определяющие конфигурацию и параметры развертывания, задаются в специальных файлах свойств, имеющих имя вида *-app.properties. Каждый блок приложения имеет набор таких файлов, включающий в себя файлы из базовых проектов платформы и файл текущего приложения. Набор файлов свойств определяется следующим образом:

  • Для блоков, являющихся веб-приложениями (Middleware, Web Client, Web Portal) набор файлов свойств задается в web.xml в параметре appPropertiesConfig.

  • Для блока Desktop Client основной способ задания набора файлов свойств − переопределение в приложении метода getDefaultAppPropertiesConfig() в классе-наследнике com.haulmont.cuba.desktop.App.

Например, набор файлов свойств блока Middleware проекта sales задается в файле web/WEB-INF/web.xml модуля core, и выглядит следующим образом:

classpath:cuba-app.properties
classpath:app.properties
file:${catalina.home}/conf/app-core/local.app.properties

Здесь префикс classpath: означает, что данный файл нужно искать в Java classpath, префикс file: − в файловой системе. Возможно использование системных свойств Java, в данном случае это catalina.home − путь к корню Tomcat.

Порядок перечисления файлов важен, так как значения, указанные в каждом последующем файле заменяют значения одноименных свойств, заданные в предыдущих файлах. Этим достигается переопределение свойств платформы в конкретном приложении.

Последний файл в приведенном наборе − local.app.properties. Он может использоваться для переопределения свойств приложения при развертывании. Если этого файла нет, он игнорируется. Если же во время инсталляции системы требуется переопределение некоторых параметров (как правило, различных URL), достаточно создать этот файл и поместить в него переопределяемые свойства. При последующих обновлениях системы такой файл с локальными настройками легко сохранить.

Аналогом 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

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

4.2.8.3. Хранение свойств в базе данных

Параметры времени выполнения хранятся в таблице SYS_CONFIG базы данных.

Такие свойства имеют следующие особенности:

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

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

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

Хранящиеся в БД свойства кэшируются на уровне Middleware. Очистить кэш можно с помощью JMX-интерфейсов ConfigStorageMBean методом clearCache() или CachingFacadeMBean методом clearConfigStorageCache().

Следует иметь в виду, что на клиентском уровне чтение свойства, хранящегося в БД, приводит к запросу к Middleware, что менее эффективно, чем чтение локального свойства из app.properties. Для уменьшения количества таких запросов клиент кэширует все свойства, хранящиеся в БД, на время жизни экземпляра реализации конфигурационного интерфейса. Поэтому если, например, в некотором экране UI необходимо несколько раз обратиться к свойствам одного конфигурационного интерфейса, лучше получить ссылку на него при инициализации экрана, и сохранить в поле для последующих обращений к одному и тому же экземпляру.

4.2.8.4. Конфигурационные интерфейсы

Данный механизм позволяет работать со свойствами приложения через методы Java-интерфейсов, что дает следующие преимущества:

  • типизированность - прикладной код работает с нужными типами (String, Boolean, Integer и пр.), а не только со строками

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

Пример получения значения таймаута транзакции в блоке Middleware:

@Inject
private ServerConfig serverConfig;

public void doSomething() {
  int timeout = serverConfig.getDefaultQueryTimeoutSec();
  ...
}

При невозможности инжекции можно получить ссылку на конфигурационный интерфейс через Configuration:

int timeout = AppBeans.get(Configuration.class)
  .getConfig(ServerConfig.class)
  .getDefaultQueryTimeoutSec();

Конфигурационные интерфейсы не являются нормальными бинами Spring, не пытайтесь получить их через AppBeans.get() - только непосредственной инжекцией самого интерфейса или через Configuration.getConfig().

4.2.8.4.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 - значение свойства будет взято из таблицы SYS_CONFIG

  • Создать методы доступа к свойству (getter / setter). Если значение свойства не предполагается изменять во время выполнения, метод доступа на запись не нужен. Возможный тип свойства рассмотрен ниже.

  • Добавить методу доступа на чтение аннотацию @Property, определяющую имя свойства.

  • Опционально аннотацию @Source можно задать для отдельного свойства в интерфейсе, если его источник отличается от заданного для всего интерфейса.

Например:

@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {

  @Property("sales.companyName")
  String getCompanyName();
}

Создавать класс реализации конфигурационного интерфейса не нужно - при получении ссылки на интерфейс через Configuration будет автоматически создан необходимый прокси-объект.

4.2.8.4.2. Типы свойств

Без дополнительных усилий поддерживаются следующие типы свойств:

  • String, простые типы либо их объектные обертки (boolean, Boolean, int, Integer, etc.).

  • Перечисления (enum). Значение свойства сохраняется в файле или БД в виде имени значения перечисления.

    Если перечисление реализует интерфейс EnumClass и имеет статический метод fromId() для получения значения по идентификатору, с помощью аннотации @EnumStore можно задать хранение значения в виде идентификатора. Например:

    @Property("myapp.defaultCustomerGrade")
    @DefaultInteger(10)
    @EnumStore(EnumStoreMode.ID)
    CustomerGrade getDefaultCustomerGrade();
    
    @EnumStore(EnumStoreMode.ID)
    void setDefaultCustomerGrade(CustomerGrade grade);
  • Классы персистентных сущностей. При обращении к свойству типа сущности происходит загрузка из БД экземпляра, заданного значением свойства.

Для поддержки произвольного типа необходимо реализовать классы TypeStringify и TypeFactory для преобразования значения в строку и из нее, и указать эти классы для свойства с помощью аннотаций @Stringify и @Factory.

Рассмотрим этот процесс на примере типа UUID.

  • Создаем класс com.haulmont.cuba.core.config.type.UuidTypeFactory унаследованный от com.haulmont.cuba.core.config.type.TypeFactory и реализуем в нем метод:

    public Object build(String string) {
      if (string == null)
          return null;
      return UUID.fromString(string);
    }
  • TypeStringify создавать не нужно, т.к. по умолчанию будет использован метод toString() − в данном случае он нам подходит.

  • Аннотируем свойство в конфигурационном интерфейсе:

    @Factory(factory = UuidTypeFactory.class)
    UUID getUuidProp();
    void setUuidProp(UUID value);

В платформе определены реализации TypeFactory для следующих типов:

  • UUID - UuidTypeFactory, описано выше.

  • java.util.Date - DateFactory. Значение даты должно быть указано в формате yyyy-MM-dd HH:mm:ss.SSS, например:

    cuba.test.dateProp = 2013-12-12 00:00:00.000
  • List<Integer> (список целых чисел) - IntegerListTypeFactory. Значение свойства должно быть указано в виде списка чисел, разделенных пробелами, например:

    cuba.test.integerListProp = 1 2 3
  • List<String> (список строк) - StringListTypeFactory. Значение свойства должно быть указано в виде списка строк, разделенных символом "|", например:

    cuba.test.stringListProp = aaa|bbb|ccc
4.2.8.4.3. Значения по умолчанию

Для свойств конфигурационных интерфейсов могут быть заданы значения по умолчанию. Эти значения будут возвращаться вместо null, если данный параметр не задан в месте хранения - в БД или в app.properties.

Значение по умолчанию может быть задано в виде строки с помощью аннотации @Default, либо в виде конкретного типа с помощью других аннотаций пакета com.haulmont.cuba.core.config.defaults:

@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();

Для сущностей значение по умолчанию задается строкой вида {entity_name}-{id}-{optional_view_name}, например:

@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();

@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();

4.2.9. Локализация сообщений

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

Возможности выбора языка пользователем определяются комбинацией свойств приложения cuba.localeSelectVisible и cuba.availableLocales.

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

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

4.2.9.1. Пакеты сообщений

Пакет сообщений представляет собой набор файлов свойств с именами вида messages{_XX}.properties, расположенных в одном Java-пакете. Суффикс XX определяет язык, для которого в данном файле содержатся сообщения, и соответствует коду языка в Locale.getLanguage(). Возможно также использование остальных атрибутов Locale, например, country. В этом случая файл пакета будет иметь вид messages{_XX_YY}.properties. Один из файлов пакета может быть без суффикса языка - это файл по умолчанию. Именем пакета сообщений считается имя Java-пакета, в котором расположены файлы пакета.

Рассмотрим пример:

/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties

Данный пакет состоит из 3-х файлов - один для русского языка, один для французского, и один по умолчанию. Имя пакета - com.abc.sales.gui.customer

Файлы сообщений содержат пары ключ-значение, где ключ - это идентификатор сообщения, на который ссылается код приложения, а значение - само сообщение на языке данного файла. Правила задания пар аналогичны правилам файлов свойств java.util.Properties, со следующими особенностями:

  • Кодировка файла - обязательно UTF-8

  • Поддерживается включение других пакетов сообщений с помощью ключа @include, в том числе нескольких сразу - перечислением через запятую. При этом если некоторый ключ сообщения встречается и во включаемом пакете, и в текущем, будет использовано сообщение из текущего. Пример включения пакетов:

    @include=com.haulmont.cuba.web, com.abc.sales.web
    
    someMessage=Some Message
    ...

Получение сообщений из пакетов производится с помощью методов интерфейса Messages по следующим правилам:

  • Сначала производится поиск в конфигурационном каталоге приложения

    • Ищется файл messages_XX.properties в каталоге, задаваемом именем пакета сообщений, где XX - код требуемого языка

    • Если такого файла нет, в этом же каталоге ищется файл по умолчанию messages.properties

    • Если найден или файл нужного языка, или файл по умолчанию, он загружается вместе со всеми @include, и в нем ищется ключ сообщения

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

  • Если в конфигурационном каталоге сообщение не найдено, производится поиск в classpath по такому же алгоритму.

  • На клиентском уровне, если сообщение не найдено на предыдущих шагах, отправляется запрос на Middleware, и сообщение ищется там аналогичным способом.

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

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

  • Если приложение не предполагает интернационализации, то можно не использовать пакеты и включать строки сообщений прямо в код приложения, либо пользоваться файлами по умолчанию messages.properties для отделения ресурсов от кода.

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

4.2.9.2. Главный пакет сообщений

Каждый стандартный блок приложения определяет для себя один главный пакет сообщений. Для блоков клиентского уровня этот пакет содержит названия пунктов главного меню и общих элементов UI (например, названия кнопок OK и Cancel). Для всех блоков приложения, включая Middleware, главный пакет определяет форматы преобразований Datatype .

Для указания главного пакета сообщений используется свойство приложения cuba.mainMessagePack . Значением свойства может быть либо один пакет, либо список пакетов, разделенный пробелами. Например:

cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web

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

4.2.9.3. Локализация названий сущностей и атрибутов

Для отображения в UI локализованных названий сущностей и их атрибутов необходимо создать специальные пакеты сообщений в тех же Java-пакетах, что и сами сущности. Формат файлов сообщений должен быть следующим:

  • Ключ названия сущности - простое имя класса (без пакета)

  • Ключ названия атрибута - простое имя класса, затем через точку имя атрибута

Пример русской локализации сущности com.abc.sales.entity.Customer - файл /com/abc/sales/entity/messages_ru.properties:

Customer=Покупатель
Customer.name=Имя
Customer.email=Email

Order=Заказ
Order.customer=Покупатель
Order.date=Дата
Order.amount=Сумма

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

  • программно - методами MessageTools getEntityCaption(), getPropertyCaption()

  • в XML дескрипторе экрана - указанием ссылки на сообщение по правилам MessageTools.loadString(): msg://{entity_package}/{key}, например,

    caption="msg://com.abc.sales.entity/Customer.name"

4.2.9.4. Локализация enum

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

  • Ключ названия перечисления - простое имя класса (без пакета)

  • Ключ значения - простое имя класса, затем через точку имя значения

Например, для перечисления

package com.abc.sales;

public enum CustomerGrade { PREMIUM, HIGH, STANDARD }

файл русской локализации /com/abc/sales/messages_ru.properties должен содержать строки:

CustomerGrade=Уровень покупателя
CustomerGrade.PREMIUM=Премиум
CustomerGrade.HIGH=Высокий
CustomerGrade.STANDARD=Стандартный

Локализованные значения перечислений автоматически используются различными визуальными компонентами, например, LookupField . Для программного получения локализованного значения перечисления можно использовать метод getMessage() интерфейса Messages , просто передавая в него экземпляр enum.

4.2.10. Аутентификация пользователей

В данном разделе рассмотрены некоторые аспекты управления доступом с точки зрения разработчика приложения. Для получения полной информации о возможностях и настройке ограничения доступа пользователей к данным см. Глава 7, Подсистема безопасности.

4.2.10.1. UserSession

Основной элемент подсистемы контроля доступа в CUBA-приложении - пользовательская сессия. Это объект класса UserSession, который ассоциирован с аутентифицированным в данный момент в системе пользователем, и содержит информацию о правах доступа пользователя к данным. Объект текущей сессии может быть получен в любом блоке приложения через интерфейс инфраструктуры UserSessionSource .

Пользовательская сессия создается на Middleware при выполнении метода LoginService.login() после аутентификации пользователя по переданному имени и паролю. Объект UserSession затем кэшируется в данном блоке Middleware, и возвращается на клиентский уровень. При работе в кластере объект сессии реплицируется на соседние узлы кластера Middleware. Клиентский блок, получив объект сессии, также сохраняет его у себя, так или иначе ассоциируя с активным пользователем (например, в HTTP сессии). Далее все вызовы Middleware для данного пользователя сопровождаются передачей идентификатора сессии (типа UUID), причем прикладному коду не нужно об этом заботиться - идентификатор сессии передается автоматически, независимо от сигнатуры вызываемых методов среднего слоя. Обработка вызовов клиентов на Middleware начинается с извлечения из кэша сессии по полученному идентификатору и установки ее в потоке выполнения. Объект сессии удаляется из кэша при вызове метода LoginService.logout(), либо при истечении времени бездействия, определяемого свойством приложения cuba.userSessionExpirationTimeoutSec .

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

Объект UserSession содержит также методы для авторизации текущего пользователя, т.е. проверки его прав на объекты системы: isScreenPermitted(), isEntityOpPermitted(), isEntityAttrPermitted(), isSpecificPermitted().

С объектом UserSession могут быть ассоциированы именованные атрибуты произвольного сериализуемого типа. Атрибуты устанавливаются методом setAttribute() и возвращаются методом getAttribute(). Последний может также возвращать следующие параметры сессии, как если бы они были атрибутами:

  • userId - ID текущего зарегистрированного или замещенного пользователя;

  • userLogin - логин текущего зарегистрированного или замещенного пользователя в нижнем регистре.

Атрибуты реплицируются в кластере Middleware так же, как и все остальные данные сессии.

4.2.10.2. Вход в систему

Стандартный вариант входа пользователя:

  • пользователь вводит свой логин и пароль

  • клиентский блок приложения хэширует пароль, вызывая метод getPlainHash() бина PasswordEncryption и вызывает на Middleware метод LoginService.login(), передавая ему логин пользователя и хэш пароля

  • LoginService делегирует выполнение бину LoginWorker, который загружает объект User по полученному логину, хэширует полученный хэш пароля повторно, используя в качестве соли идентификатор пользователя, и сравнивает полученный хэш с сохраненным в БД хэшем пароля. В случае несовпадения выбрасывается исключение LoginException.

  • После успешной аутентификации в созданный экземпляр UserSession загружаются все параметры доступа данного пользователя: список ролей, права, ограничения и атрибуты сессии.

Алгоритм хэширования паролей реализуется бином типа EncryptionModule и задается в свойстве приложения cuba.passwordEncryptionModule . По умолчанию - SHA-1.

Возможен вариант, когда пароль пользователя (точнее, хэш пароля) не хранится в базе данных, а проверяется внешними средствами, например, путем интеграции с ActiveDirectory. В этом случае фактически аутентификацию выполняет клиентский блок, а Middleware "доверяет" клиенту, создавая сессию по одному только логину пользователя без пароля методом LoginService.loginTrusted(). Метод loginTrusted() требует выполнения следующих условия:

  • клиентский блок должен передать так называемый доверенный пароль, задаваемый на Middleware и на клиентском блоке свойством приложения cuba.trustedClientPassword

  • IP адрес клиентского блока должен соответствовать маске, задаваемой свойством приложения cuba.trustedClientPermittedIpMask

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

Дополнительным плюсом входа в систему для автоматического процесса и для JMX-вызова является то, что вывод в журнал сообщений от логгеров сопровождается указанием логина текущего пользователя, если пользовательская сессия установлена в потоке выполнения. Это упрощает поиск сообщений от конкретного процесса при разборе журнала.

Вход в систему для процессов внутри Middleware выполняется вызовом LoginWorker.loginSystem() с передачей логина пользователя (без пароля), от имени которого будет работать данный процесс. В результате создается объект UserSession , который будет закэширован в данном блоке Middleware и не будет реплицироваться в кластере.

Более подробно аутентификация процессов внутри Middleware рассмотрена в разделе Раздел 4.4.2, «Системная аутентификация»

4.2.10.3. SecurityContext

Экземпляр класса SecurityContext хранит информацию о пользовательской сессии для текущего потока выполнения. Он создается и передается в метод AppContext.setSecurityContext() в следующие моменты:

  • для блоков Web Client и Web Portal - в начале обработки каждого HTTP-запроса от пользовательского браузера

  • для блока Middleware - в начале обработки каждого запроса от клиентского уровня

  • для блока Desktop Client - один раз после входа пользователя, так как десктопное приложение является однопользовательским

По окончании выполнения запроса в первых двух случаях SecurityContext удаляется из потока выполнения.

При создании прикладным кодом нового потока выполнения в него необходимо передать текущий экземпляр SecurityContext, например:

final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
  public void run() {
      AppContext.setSecurityContext(securityContext);
      // business logic here
  }
});

4.2.11. Обработка исключений

В данном разделе рассмотрены различные аспекты генерации и обработки исключений в CUBA-приложениях.

4.2.11.1. Классы исключений

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

  • Если исключение является нормальной частью бизнес-логики и при его возникновении требуется предпринимать некоторые нетривиальные действия, то класс исключения следует делать декларируемым (наследником Exception). Обработка таких исключений производится вызывающим кодом.

  • Если исключение сигнализирует об ошибочной ситуации, и реакцией на него должно быть прерывание хода выполнения и простое действие типа отображения информации об ошибке пользователю, то класс исключения следует делать недекларируемым (наследником RuntimeException). Обработка таких исключений производится специальными классами-обработчиками, зарегистрированными в клиентских блоках приложения.

  • Если исключение выбрасывается и обрабатывается в рамках одного блока приложения, то класс исключения следует объявлять в соответствующем модуле. Если же исключение выбрасывается на Middleware, а обрабатывается на клиентском уровне, то класс исключения необходимо объявлять в модуле global.

Платформа содержит специальный класс недекларируемого исключения SilentException, который можно использовать для прерывания хода выполнения без выдачи каких-либо сообщений пользователю или в лог. SilentException объявлен в модуле global, поэтому доступен как на Middleware, так и в клиентских блоках.

4.2.11.2. Передача исключений Middleware

Если при выполнении запроса от клиента на Middleware возникает исключение, выполнение прерывается и на клиента возвращается объект исключения, как правило, включающий цепочку порождающих друг друга исключений. Так как цепочка исключений может содержать классы, недоступные клиентскому блоку (например, исключения JDBC-драйвера), на клиента передается не сама эта цепочка, а ее представление внутри специального создаваемого исключения RemoteException.

Информация об исключениях-причинах сохраняется в виде списка объектов RemoteException.Cause. Каждый объект Cause хранит обязательно имя класса исключения и его сообщение. Кроме того, если класс исключения "поддерживается клиентом", то Cause содержит также и сам объект исключения. Это дает возможность передать на клиента информацию в полях исключения.

Класс исключения, объекты которого нужно передавать на клиентский уровень именно в виде Java-объектов, нужно аннотировать @SupportedByClient, например:

@SupportedByClient
public class WorkflowException extends RuntimeException {
...

Таким образом, при возникновении на Middleware исключения, не аннотированного @SupportedByClient, вызывающий клиентский код получит RemoteException, внутри которого будет находиться исходное исключение в виде строки. Если же исходное исключение аннотировано @SupportedByClient, то вызывающий код получит именно его. Это дает возможность в прикладном коде организовывать обработку декларируемых сервисами 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 {
...

4.2.11.3. Обработчики исключений клиентского уровня

Необработанные исключения в блоках Web Client и Desktop Client, возникшие на клиентском уровне или переданные с Middleware, попадают в специальный механизм обработчиков. Этот механизм реализован в модуле GUI и доступен обоим клиентам.

Обработчик должен быть управляемым бином, реализовывать интерфейс GenericExceptionHandler, в методе handle() которого производить обработку и возвращать true, либо сразу возвращать false, если данный обработчик не может обработать переданное ему исключение. Такое поведение позволяет организовать "цепочку ответственности" обработчиков.

Рекомендуется наследовать классы своих обработчиков от базового класса AbstractGenericExceptionHandler, который умеет разбирать цепочку исключений (с учетом упакованных внутри RemoteException) и реагировать на конкретные типы исключений. Типы исключений, для которых предназначен данный обработчик, указываются в массиве строк, передаваемом в конструкторе обработчика базовому конструктору. Каждая строка массива должна содержать одно полное имя класса обрабатываемого исключения, например:

@ManagedBean("cuba_EntityAccessExceptionHandler")
public class EntityAccessExceptionHandler extends AbstractGenericExceptionHandler {

    public EntityAccessExceptionHandler() {
        super(EntityAccessException.class.getName());
    }
...

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

@ManagedBean("cuba_OptimisticExceptionHandler")
public class OptimisticExceptionHandler extends AbstractGenericExceptionHandler implements Ordered {

    public OptimisticExceptionHandler() {
        super("org.springframework.orm.jpa.JpaOptimisticLockingFailureException");
    }
...

В случае использования в качестве базового класса AbstractGenericExceptionHandler логика обработки располагается в методе doHandle(), и может выглядеть следующим образом:

@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, если данный обработчик применим для исключения. Например:

@ManagedBean("cuba_NumericOverflowExceptionHandler")
public class NumericOverflowExceptionHandler extends AbstractGenericExceptionHandler {

    public NumericOverflowExceptionHandler() {
        super(ReportingSQLException.class.getName());
    }

    @Override
    protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
        return StringUtils.containsIgnoreCase(message, "Numeric field overflow");
    }
...

4.3. Компоненты работы с базой данных

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

Компоненты работы с базой данных принадлежат блоку Middleware, другие блоки приложения не имеют прямого доступа к БД.

Дополнительная практическая информация по работе с базой данных приведена в Раздел 5.5, «Проектирование БД» и Раздел 6.5, «Создание и обновление БД при эксплуатации приложения».

4.3.1. Типы СУБД

Тип используемой СУБД определяется свойствами приложения cuba.dbmsType и (опционально) cuba.dbmsVersion, а также настройкой источника данных javax.sql.DataSource, через который производится обращение к базе данных. Экземпляр источника данных извлекается из JNDI по имени, заданному в свойстве приложения cuba.dataSourceJndiName . Конфигурационный файл для Tomcat, определяющий источник данных, описан в Раздел A.1, «context.xml»

Платформа "из коробки" поддерживает следующие СУБД:

 cuba.dbmsTypecuba.dbmsVersion
HSQLDBhsql 
PostgreSQL 8.4+postgres 
Microsoft SQL Server 2005, 2008mssql 
Microsoft SQL Server 2012+mssql2012
Oracle Database 11goracle 

Таблица ниже описывает рекомендованное соответствие типов данных между атрибутами сущностей в Java и колонками таблиц различных СУБД. Эти типы автоматически выбираются Studio при генерации скриптов создания и обновления БД, и для них гарантируется работоспособность всех механизмов платформы.

JavaHSQLPostgreSQLMS SQL ServerOracle
UUIDvarchar(36)uuiduniqueidentifiervarchar2(32)
Datetimestamptimestampdatetimetimestamp
java.sql.Datetimestampdatedatetimedate
java.sql.Timetimestamptimedatetimetimestamp
BigDecimaldecimal(p, s)decimal(p, s)decimal(p, s)number(p, s)
Doubledouble precisiondouble precisiondouble precisionfloat
Longbigintbigintbigintnumber(19)
Integerintegerintegerintegerinteger
Booleanbooleanbooleantinyintchar(1)
String (limited)varchar(n)varchar(n)varchar(n)varchar2(n)
String (unlimited)longvarchartextvarchar(max)clob
byte[]longvarbinarybyteaimageblob

Как правило, всю работу по преобразованию данных между БД и кодом Java выполняет слой ORM совместно с соответствующим JDBC драйвером. Это означает, что при работе с данными через методы EntityManager и запросы на JPQL никакой ручной конвертации выполнять не нужно - вы просто используете типы Java, перечисленные в левой колонке таблицы.

При использовании native SQL через EntityManager.createNativeQuery() или через QueryRunner для разных типов СУБД некоторые типы данных в Java коде будут отличаться от приведенных. В первую очередь это касается атрибутов типа UUID - только драйвер PostgreSQL возвращает значения соответствующих колонок в этом типе, для других серверов это будет String. Для обеспечения независимости кода от используемой СУБД рекомендуется конвертировать типы параметров и результатов запросов с помощью интерфейса DbTypeConverter .

4.3.1.1. Поддержка произвольных СУБД

На уровне прикладного проекта можно реализовать работу с любой СУБД, поддерживаемой фреймворком ORM (OpenJPA). Для этого достаточно выполнить следующее:

  • Указать тип СУБД в виде произвольного кода в свойстве cuba.dbmsType. Код должен отличаться от используемых в платформе кодов hsql, postgres, mssql, oracle.

  • Реализовать интерфейсы DbmsFeatures, SequenceSupport, DbTypeConverter классами с именами соответственно TypeDbmsFeatures, TypeSequenceSupport, TypeDbTypeConverter, где Type - код типа СУБД. Пакет класса имплементации должен быть таким же, как у интерфейса.

  • Если проект включает базовый проект Workflow, необходимо переопределить бин CubaJbpmSpringHelper и его метод getHibernateDialectName() для выбора диалекта Hibernate, используемого в jBPM.

  • Создать скрипты инициализации и обновления БД в каталогах с кодом СУБД. Скрипты инициализации должны включать создание всех объектов БД, необходимых для сущностей платформы (их можно скопировать из имеющихся в каталоге 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'                                            // 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'
    }

4.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.Mssql2012SequenceSupport, который вступит в силу, если в проекте указаны следующие свойства:

cuba.dbmsType = mssql
cuba.dbmsVersion = 2012

При поиске скриптов создания и обновления БД каталог с именем type-version имеет приоритет над каталогом с именем type. Это значит, что скрипты каталога type-version заменяют одноименные скрипты каталога type. В каталоге type-version могут быть и скрипты с собственными именами, они будут также добавлены в общий набор скриптов для выполнения. Сортировка скриптов производится по пути начиная с каталога, вложенного в type или type-version, то есть без учета того, в каком каталоге (с версией или без) находится скрипт.

Например, следующим образом можно определить скрипты создания БД для Microsoft SQL Server для версий ниже и выше 2012:

modules/core/db/init/
   mssql/
       10.create-db.sql
       20.create-db.sql
       30.create-db.sql
   mssql-2012/
       10.create-db.sql 

4.3.2. Скрипты создания и обновления БД

Проект CUBA-приложения всегда содержит два набора скриптов:

  • Скрипты создания БД, предназначенные для создания базы данных с нуля. Они содержат набор DDL и DML операторов, после выполнении которых на пустой БД схема базы данных полностью соответствует текущему состоянию модели данных приложения. Скрипты создания могут также наполнять БД необходимыми первичными данными.

  • Скрипты обновления БД - предназначены для поэтапного приведения структуры БД к текущему состоянию модели данных.

При изменении модели данных необходимо отразить соответствующее изменение схемы БД и в скриптах содания, и в скриптах обновления. Например, при добавлении атрибута address в сущность Customer, нужно:

  1. Изменить оператор создания таблицы в скрипте создания:

    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)
    )
  2. Добавить скрипт обновления, содержащий оператор модификации таблицы:

    alter table SALES_CUSTOMER add ADDRESS varchar(200)

Скрипты создания располагаются в каталоге /db/init модуля core. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType , например, /db/init/postgres. Имена скриптов создания должны иметь вид {optional_prefix}create-db.sql.

Скрипты обновления располагаются в каталоге /db/update модуля core. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType , например, /db/update/postgres.

Скрипты обновления могут быть двух типов: с расширением *.sql или с расширением *.groovy. SQL-скрипты являются основным средством обновления базы данных. Groovy-скрипты выполняются только механизмом запуска скриптов БД сервером, поэтому применяются в основном на этапе эксплуатации приложения - как правило, это процессы миграции или импорта данных, которые невозможно реализовать на SQL.

Скрипты обновления должны иметь имена, которые при сортировке в алфавитном порядке образуют правильную последовательность их выполнения (обычно это хронологическая последовательность их создания). Поэтому при ручном создании рекомендуется задавать имя скрипта обновления в виде {yymmdd}-{description}.sql, где yy - год, mm - месяц, dd - день, description - краткое описание скрипта. Например, 121003-addCodeToCategoryAttribute.sql. Studio при автоматической генерации скриптов также придерживается этого формата.

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

В развернутом приложении скрипты создания и обновления БД располагаются в специальном каталоге скриптов базы данных, задаваемым свойством приложения cuba.dbDir .

4.3.2.1. Структура SQL-скриптов

SQL-скрипты создания и обновления представляют собой текстовые файлы с набором DDL и DML команд, разделенных символом "^". Символ "^" применяется для того, чтобы можно было применять разделитель ";" в составе сложных команд, например, при создании функций или триггеров. Механизм исполнения скриптов разделяет входной файл на команды по разделителю "^" и выполняет каждую команду в отдельной транзакции. Это означает, что при необходимости можно сгруппировать несколько простых операторов (например, insert), разделенных точкой с запятой, и обеспечить их выполнение в одной транзакции.

Пример SQL-скрипта обновления:

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)^

4.3.2.2. Структура Groovy-скриптов

Groovy-скрипты обновления имеют следующую структуру:

  • Основная часть, содержащая код, выполняемый до старта контекста приложения. В этой части можно использовать любые классы Java, Groovy и блока Middleware приложения, но при этом необходимо иметь в виду, что никакие бины, интерфейсы инфраструктуры и прочие объекты приложения еще не инстанциированы, и с ними работать нельзя.

    Основная часть предназначена в первую очередь, как и обычные SQL-скрипты, для обновления схемы данных.

  • PostUpdate часть - набор замыканий, которые будут выполнены после завершения процесса обновления и после старта контекста приложения. Внутри этих замыканий можно оперировать любыми объектами Middleware приложения.

    В этой части скрипта удобно, напимер, выполнять импорт данных, так как в ней можно использовать интерфейс 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({
  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 Color()
      c.name = 'yellow'
      c.description = 'a description'

      em.persist(c)
      tr.commit()
  } finally {
      tr.end()
  }
})

4.3.3. Выполнение скриптов БД задачами Gradle

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

Для запуска скриптов создания БД служит задача createDb. В Studio ей соответствует команда главного меню Run -> Create database. При запуске задачи происходит следующее:

  1. В каталоге modules/core/build/db собираются скрипты базовых проектов платформы и скрипты db/**/*.sql модуля core текущего проекта. Наборы скриптов базовых проектов располагаются в подкаталогах с числовыми префиксами начиная с 10, скрипты текущего проекта - в подкаталоге с префиксом 50. Числовые префиксы необходимы для соблюдения алфавитного порядка выполнения скриптов - сначала выполняются скрипты cuba, затем других базовых проектов, затем текущего проекта.

  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 ей соответствует команда главного меню Run -> Update database. При запуске задачи происходит следующее:

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

  2. Производится проверка, все ли базовые проекты имеют необходимые таблицы в базе данных. Если обнаруживается, что БД не инициализирована для работы некоторого базового проекта, выполняются его скрипты создания.

  3. В каталогах modules/core/build/db/update/** производится поиск скриптов обновления, не зарегистрированных в таблице SYS_DB_CHANGELOG, то есть не выполненных ранее и содержимое которых не отражено в БД при ее инициализации.

  4. Последовательно в алфавитном порядке выполняются все найденные на предыдущем шаге скрипты, и их имена вместе с путем относительно каталога db регистрируются в таблице SYS_DB_CHANGELOG.

4.3.4. Выполнение скриптов БД сервером

Механизм выполнения скриптов сервером предназначен для приведения БД в актуальное состояние на старте сервера приложения, и активируется во время инициализации блока Middleware. Понятно, что при этом приложение должно быть собрано и развернуто на сервере, будь то собственный 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, т.е. не запускавшиеся ранее. Последовательность запуска скриптов определяется 2-мя факторами: приоритетом базового проекта (см. содержимое каталога скриптов базы данных: 10-cuba, 20-workflow, ...) и именем файла скрипта (с учетом подкаталогов внутри каталога update) в алфавитном порядке.

    Перед выполнением скриптов обновления производится проверка, все ли базовые проекты имеют необходимые таблицы в базе данных. Если обнаруживается, что БД не инициализирована для работы некоторого базового проекта, выполняются его скрипты создания.

Механизм выполнения скриптов на старте сервера включается свойством приложения cuba.automaticDatabaseUpdate .

В запущенном приложении механизм выполнения скриптов можно стартовать с помощью JMX-бина app-core.cuba:type=PersistenceManager, вызвав его метод updateDatabase() с параметром update. Понятно, что таким способом можно только обновить БД, а не проинициализировать новую, так как войти в систему для запуска метода JMX-бина при пустой БД невозможно. При этом следует иметь в виду, что если на старте Middleware или при входе пользователя в систему начнется инициализация той части модели данных, которая уже не соответствует устаревшей схеме БД, то произойдет ошибка, и продолжение работы станет невозможным. Именно поэтому универсальным является только автоматическое обновление БД на старте сервера перед инициализацией модели данных.

JMX-бин app-core.cuba:type=PersistenceManager имеет еще один метод, относящийся к механизму обновления БД: findUpdateDatabaseScripts(). Он возвращает список новых скриптов обновления, имеющихся в каталоге и не зарегистрированных в БД.

Практические рекомендации по использованию механизма обновления БД сервером приведены в Раздел 6.5, «Создание и обновление БД при эксплуатации приложения».

4.4. Компоненты среднего слоя

На следующем рисунке приведены основные компоненты среднего слоя CUBA-приложения.

Рисунок 13. Компоненты среднего слоя

Компоненты среднего слоя

Services – управляемые контейнером компоненты, формирующие границу приложения и предоставляющие интерфейс клиентскому уровню приложения. Сервисы могут содержать бизнес-логику сами, либо делегировать выполнение Managed Beans.

Managed Beans – управляемые контейнером компоненты, содержащие бизнес-логику приложения. Вызываются сервисами, другими бинами или через опциональный JMX интерфейс.

Persistence − инфраструктурный интерфейс для доступа к функциональности хранения данных: управлению транзакциями и ORM.

4.4.1. Сервисы

Сервисы образуют слой компонентов, определяющий множество операций Middleware, доступных клиентскому уровню приложения. Внутри сервисов инкапсулируется бизнес-логика и управление транзакциями.

Основные задачи сервисов:

  • Предоставляют удаленный (remote) интерфейс для вызова с клиентского уровня

  • Проверяют наличие активной пользовательской сессии, соответствующей идентификатору сессии, переданному с клиента

  • Записывают в журнал необработанные исключения среднего слоя

Кроме того, именно в слое сервисов рекомендуется выполнять авторизацию текущего пользователя, т.е. проверять его права на ту или иную функциональность.

Общие для всех сервисов задачи решаются следующим образом:

  • Проверка наличия пользовательской сессии и логгирование исключений производится классом-интерцептором ServiceInterceptor, который перехватывает выполнение каждого метода сервиса с помощью Spring AOP

  • Удаленный интерфейс для доступа к сервису через Spring HTTP Invoker создается бином RemoteServicesBeanCreator, который конфигурируется в файле remoting-spring.xml модуля core.

Рисунок 14. Диаграмма классов сервиса

Диаграмма классов сервиса

4.4.1.1. Создание сервиса

Имена интерфейсов сервисов должны заканчиваться на Service, имена классов реализации на ServiceBean.

При создании сервиса необходимо выполнить следующее:

  1. Создать интерфейс в модуле global (т.к. интерфейс сервиса должен быть доступен на всех уровнях) и задать в нем имя сервиса. Имя рекомендуется задавать в формате {имя_проекта}_{имя_интерфейса}. Например:

    package com.sample.sales.core;
    
    import com.sample.sales.entity.Order;
    
    public interface OrderService {
        String NAME = "sales_OrderService";
    
        void calculateTotals(Order order);
    }
  2. Создать класс сервиса в модуле core и добавить ему аннотацию @org.springframework.stereotype.Service с именем, заданным в интерфейсе

    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 содержит элемент:

    <context:component-scan base-package="com.sample.sales"/>

    что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.

Сервисы предназначены только для вызова "снаружи" Middleware. Не рекомендуется вызывать методы сервисов из других компонентов среднего слоя. При обнаружении факта вызова сервиса из другого сервиса в журнал выводится сообщение об ошибке.

Если некоторую бизнес-логику требуется вызывать из разных сервисов либо других компонентов Middleware, ее необходимо выделить и инкапсулировать внутри соответствующего Managed Bean.

4.4.1.2. Использование сервиса

Для того чтобы вызывать сервис, в клиентском блоке приложения для него должен быть создан соответствующий прокси-объект. Делается это путем объявления имени и интерфейса сервиса в параметрах фабрики прокси-объектов. Для блока Web Client это бин класса WebRemoteProxyBeanCreator, для Web Portal - PortalRemoteProxyBeanCreator , для Desktop Client - RemoteProxyBeanCreator .

Фабрика прокси-объектов конфигурируется в файле spring.xml соответствующего клиентского блока.

Например, чтобы в приложении sales вызвать с веб-клиента сервис sales_OrderService, необходимо добавить в файл web-spring.xml модуля web следующее:

<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
    <property name="clusterInvocationSupport" ref="cuba_clusterInvocationSupport"/>
    <property name="remoteServices">
        <map>
            <entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
        </map>
    </property>
</bean>

Все импортируемые сервисы объявляются в одном свойстве remoteServices в элементах map/entry.

С точки зрения прикладного кода прокси-объект сервиса на клиентском уровне является обычным бином Spring и может быть получен либо инжекцией, либо с помощью класса AppBeans, например:

@Inject
protected OrderService orderService;
...
orderService.calculateTotals(order);

4.4.1.3. DataService

DataService является фасадом для вызова серверной релизации DataManager с клиентского уровня. DataService не рекомендуется использовать в прикладном коде. Вместо него и на клиентском уровне, и на Middleware следует использовать DataManager.

4.4.2. Системная аутентификация

При выполнении пользовательских запросов программному коду Middleware через интерфейс UserSessionSource всегда доступна информация о текущем пользователе. Это возможно потому, что при получении запроса с клиентского уровня в потоке выполнения автоматически устанавливается соответствующий объект SecurityContext .

Однако существуют ситуации, когда текущий поток выполнения не связан ни с каким пользователем системы: например, при вызове метода бина из планировщика, либо через JMX-интерфейс. Если при этом бин выполняет изменение сущностей в базе данных, то ему потребуется информация о том, кто выполняет изменения, то есть аутентификация.

Такого рода аутентификация называется системной, так как не требует участия пользователя - средний слой приложения просто создает (или использует имеющуюся) пользовательскую сессию, и устанавливает в потоке выполнения соответствующий объект SecurityContext.

Обеспечить системную аутентификацию некоторого участка кода можно следующими способами:

  • явно используя бин com.haulmont.cuba.security.app.Authentication, например:

    @Inject
    protected Authentication authentication;
    ...
    authentication.begin();
    try {
        // authenticated code
    } finally {
        authentication.end();
    }
  • добавив методу бина аннотацию @Authenticated, например:

    @Authenticated
    public String foo(String value) {
        // authenticated code
    }

Во втором случае также используется бин Authentication, но неявно, через интерцептор AuthenticationInterceptor, который перехватывает вызовы всех методов бинов с аннотацией @Authenticated.

В приведенных примерах пользовательская сессия будет создаваться от лица пользователя, логин которого указан в свойстве приложения cuba.jmxUserLogin . Если требуется аутентификация от имени другого пользователя, нужно воспользоваться первым вариантом и передать в метод begin() логин нужного пользователя.

Если в момент выполнения Authentication.begin() в текущем потоке выполнения присутствует активная пользовательская сессия, то она не заменяется - соответственно, код, требующий аутентификации, будет выполняться с имеющейся сессией, и последующий метод end() не будет очищать поток.

Например, вызов метода JMX-бина из встроенной в Web Client консоли JMX, если бин находится в той же JVM, что и блок WebClient, к которому в данный момент подключен пользователь, будет выполнен от имени текущего зарегистрированного в системе пользователя, независимо от наличия системной аутентификации.

4.4.3. Интерфейс Persistence

Интерфейс инфраструктуры, являющийся точкой входа в функциональность хранения данных в БД.

Методы интерфейса:

  • createTransaction(), getTransaction() - получить интерфейс управления транзакциями

  • isInTransaction() - определяет, существует ли в данный момент активная транзакция

  • getEntityManager() - возвращает экземпляр EntityManager для текущей транзакции

  • isSoftDeletion() - позволяет определить, активен ли режим мягкого удаления

  • setSoftDeletion() - устанавливает или отключает режим мягкого удаления. Влияет на аналогичный признак всех создаваемых экземпляров EntityManager. По умолчанию мягкое удаление включено.

  • getDbTypeConverter() - возвращает экземпляр DbTypeConverter для используемой в данный момент базы данных.

  • getDataSource() - получить javax.sql.DataSource для используемой в данный момент базы данных.

    Для всех объектов javax.sql.Connection, получаемых методом getDataSource().getConnection(), необходимо после использования соединения вызвать метод close() в секции finally. В противном случае соединение не вернется в пул, через какое-то время пул переполнится, и приложение не сможет выполнять запросы к базе данных.

  • getTools() - возвращает экземпляр интерфейса PersistenceTools (см. ниже).

4.4.3.1. PersistenceTools

ManagedBean, содержащий вспомогательные методы работы с хранилищем данных. Интерфейс PersistenceTools можно получить либо методом Persistence.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.

Методы PersistenceTools:

  • getDirtyFields() - возвращает коллекцию имен атрибутов сущности, измененных со времени последней загрузки экземпляра из БД. Для новых экземпляров возвращает пустую коллекцию.

  • isLoaded() - определяет, загружен ли из БД указанный атрибут экземпляра. Атрибут может быть не загружен, если он не указан в примененном при загрузке представлении.

    Данный метод работает только для экземпляров в состоянии Managed.

  • getReferenceId() - возвращает идентификатор связанной сущности без загрузки ее из БД.

    Предположим, в персистентный контекст загружен экземпляр Order, и нужно получить значение идентификатора экземпляра Customer, связанного с данным Заказом. Стандартное решение order.getCustomer().getId() приведет к выполнению SQL запроса к БД для загрузки экземпляра Customer, что в данном случае избыточно, так как значение идентификатора Покупателя физически находится также и в таблице Заказов. Выполнение же

    persistence.getTools().getReferenceId(order, "customer")

    не вызовет никаких дополнительных запросов к базе данных.

    Данный метод работает только для экземпляров в состоянии Managed.

Для расширения набора вспомогательных методов в конкретном приложении бин PersistenceTools можно переопределить. Примеры работы с расширенным интерфейсом:

MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();

4.4.3.2. PersistenceHelper

Вспомогательный класс для получения информации о персистентных сущностях. В отличие от бинов Persistence и PersistenceTools доступен на всех уровнях приложения.

Методы PersistenceHelper:

  • isNew() - определяет, является ли переданный экземпляр только что созданным, т.е. находящимся в состоянии New. Возвращает true, также если экземпляр не является персистентной сущностью.

  • isDetached() - определяет, находится ли переданный экземпляр в состоянии Detached. Возвращает true, также если экземпляр не является персистентной сущностью.

  • isSoftDeleted() - определяет, поддерживает ли переданный класс сущности мягкое удаление

  • getEntityName() - возвращает имя сущности, заданное в аннотации @Entity

  • getTableName() - возвращает имя таблицы БД, хранящей экземпляры сущности, заданное в аннотации @Table

4.4.3.3. DbTypeConverter

Интерфейс, определяющий методы для конвертации данных между значениями атрибутов модели данных и параметрами и результатами запросов JDBC. Объект данного интерфейса можно получить методом Persistence.getDbTypeConverter().

Методы DbTypeConverter:

  • getJavaObject() - конвертирует результат JDBC запроса в тип, подходящий для присвоения атрибуту сущности.

  • getSqlObject() - конвертирует значение атрибута сущности в тип, подходящий для присвоения параметру JDBC запроса.

  • getSqlType() - возвращает константу из java.sql.Types, соответствующую переданному типу атрибута сущности.

4.4.4. Слой ORM

Object-Relational Mapping - объектно-реляционное отображение - технология связывания таблиц реляционной базы данных с объектами языка программирования.

Преимущества использования ORM:
  • Позволяет работать с данными реляционной СУБД, манипулируя объектами Java

  • Упрощает программирование, избавляя от рутины написания тривиальных SQL-запросов

  • Упрощает программирование, позволяя извлекать и сохранять целые графы объектов одной командой

  • Обеспечивает легкое портирование приложения на различные СУБД

  • Использует лаконичный язык запросов JPQL

  • Оптимизирует количество выполняемых SQL-запросов на команды insert и update

Недостатки:
  • Требует понимания особенностей работы с ORM

  • Не позволяет напрямую оптимизировать SQL или использовать особенности применяемой СУБД

В платформе CUBA используется реализация ORM по стандарту Java Persistence API на основе фреймворка Apache OpenJPA.

4.4.4.1. EntityManager

EntityManager - основной интерфейс ORM, служит для управления персистентными сущностями.

Ссылку на 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() - загружает экземпляр сущности по идентификатору.

    При формировании запроса к БД учитывается представление, переданное в параметре данного метода, либо установленное для всего EntityManager методом setView(). В результате в персистентном контексте окажется граф объектов, для которого загружены все не-lazy атрибуты представления. Остальные атрибуты можно дозагрузить обращением к соответствующим методам доступа объектов, либо вызовом метода fetch().

  • createQuery() - создать объект Query для выполнения JPQL запроса.

    Рекомендуется использовать вариант метода с передачей класса сущности для получения экземпляра TypedQuery.

  • createNativeQuery() - создать объект Query для выполнения SQL запроса.

  • setView() - устанавливает представление по умолчанию, с которым будет производиться последующая загрузка сущностей методом find() либо JPQL запросами. В результате жадно загружены будут все не-lazy атрибуты представления.

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

    Представления, явно переданные в метод find() или установленные в объекте Query имеют приоритет над установленным данным методом.

  • addView() - аналогичен методу setView(), но в случае наличия уже установленного в EntityManager представления, не заменяет его, а добавляет атрибуты переданного представления.

  • fetch() - обеспечивает для экземпляра сущности загрузку всех атрибутов указанного представления, включая lazy атрибуты. Экземпляр сущности должен быть в Managed состоянии.

    Данный метод рекомендуется вызывать перед коммитом транзакции, если представление содержит lazy атрибуты, а экземпляр сущности нужно отправить на клиентский уровень. В этом случае только после вызова fetch() можно быть уверенным, что все нужные клиентсткому коду атрибуты действительно загружены.

  • reload() - перезагрузить экземпляр сущности с указанным представлением. Обеспечивает загрузку всех атрибутов представления, вызывая внутри себя метод fetch().

  • isSoftDeletion() - проверяет, находится ли данный EntityManager в режиме мягкого удаления.

  • setSoftDeletion() - устанавливает режим мягкого удаления для данного экземпляра EntityManager.

  • getConnection() - возвращает java.sql.Connection, через который выполняет запросы данный экземпляр EntityManager, и, соответственно, текущая транзакция. Закрывать такое соединение не нужно, оно будет закрыто при завершении транзакции.

  • getDelegate() - возвращает javax.persistence.EntityManager, предоставляемый реализацией ORM.

4.4.4.2. Состояния сущности

New

Только что созданный в памяти экземпляр, например: Car car = new Car()

Новый экземпляр может быть передан в EntityManager.persist() для сохранения в БД, при этом он переходит в состояние Managed.

Managed

Загруженный из БД или новый, переданный в EntityManager.persist(), экземпляр. Принадлежит некоторому экземпляру EntityManager, другими словами, находится в его персистентном контексте.

Любые изменения экземпляра в состоянии Managed будут сохранены в БД в случае коммита транзакции, к которой принадлежит данный EntityManager

Detached

Экземпляр, загруженный из БД и отсоединенный от своего персистентного контекста (вследствие закрытия транзакции или сериализации).

Изменения, вносимые в Detached экземпляр, запоминаются в самом этом экземпляре (в полях, добавленных с помощью bytecode enhancement). Эти изменения будут сохранены в БД, только если данный экземпляр будет снова переведен в состояние Managed путем передачи в метод EntityManager.merge().

4.4.4.3. Загрузка по требованию

Загрузка по требованию (lazy loading) позволяет загружать связанные сущности отложенно, т.е. только в момент первого обращения к их свойствам.

Загрузка по требованию в сумме порождает больше запросов к БД, чем жадная загрузка (eager fetching), однако нагрузка при этом растянута во времени.

  • Например, при извлечении списка N экземпляров сущности A, содержащих ссылку на экземпляр сущности B, в случае загрузки по требованию будет выполнено N+1 запросов к базе данных.

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

  • Если A содержит коллекцию B, в случае жадной загрузки ORM сформирует SQL запрос, возвращающий произведение строк A и B.

  • Иногда загрузка по требованию с точки зрения производительности предпочтительнее, чем жадная загрузка. Например, когда работает асинхронный процесс, выполняющий некоторую бизнес-логику, общее время выполнения некритично и желательно распределить во времени нагрузку на БД.

Загрузка по требованию работает только для экземпляра в состоянии Managed, то есть внутри транзакции, загрузившей данный экземпляр.

4.4.4.4. Выполнение JPQL запросов

Для выполнения JPQL запросов предназначен интерфейс Query, ссылку на который можно получить у текущего экземпляра EntityManager вызовом метода createQuery(). Если запрос предполагается использовать для извлечения сущностей, рекомендуется вызывать createQuery() с передачей типа результата, что приведет к созданию TypedQuery.

Методы Query в основном соответствуют методам стандартного интерфейса javax.persistence.Query . Рассмотрим отличия.

  • setParameter() - устанавливает значение параметра запроса. При передаче в данный метод экземпляра сущности выполняет неявное преобразование экземпляра в его идентификатор. Например:

    Customer customer = ...;
    TypedQuery<Order> query = entityManager.createQuery(
        "select o from sales$Order o where o.customer.id = ?1", Order.class);
    query.setParameter(1, customer);

    Обратите внимание на сравнение в запросе по идентификатору, но передачу в качестве параметра самого экземпляра сущности.

    Вариант метода с передачей implicitConversions = false не выполняет такого преобразования.

  • setView(), addView() - аналогичны одноименным методам интерфейса EntityManager - устанавливают представление, используемое при загрузке данных текущим запросом, не влияя на представление всего EntityManager.

  • getDelegate() - возвращает экземпляр javax.persistence.Query, предоставляемый реализацией ORM.

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

TypedQuery<User> query;
List<User> list;

query = em.createQuery("select u from sec$User u where u.name = ?1", User.class);
query.setParameter(1, "testUser");
list = query.getResultList();
assertEquals(1, list.size());
User user = list.get(0);

user.setName("newName");

query = em.createQuery("select u from sec$User u where u.name = ?1", User.class);
query.setParameter(1, "testUser");
list = query.getResultList();
assertEquals(1, list.size());
User user1 = list.get(0);

assertTrue(user1 == user);

Такое поведение определяется параметром openjpa.IgnoreChanges=true, заданным в файле persistence.xml базового проекта cuba. В прикладном проекте данный параметр можно изменить, указав его в собственном persistence.xml.

Запросы, модифицирующие данные (update, delete) приводят к сбросу (flush) в базу данных текущего персистентного контекста перед выполнением. Другими словами, ORM сначала синхронизирует состояние сущностей в персистентном контексте и в БД, а уже потом выполняет модифицирующий запрос. Рекомендуется выполнять такие запросы в неизмененном персистентном контексте, чтобы исключить неявные действия ORM, которые могут отрицательно сказаться на производительности.

4.4.4.4.1. Поиск подстроки без учета регистра

Для удобного формирования условия поиска без учета регистра символов и по любой части строки можно использовать префикс (?i) в значении параметра запроса. Например, имеется запрос:

select c from sales$Customer c where c.name like :name

Если в значении параметра name передать строку (?i)%doe%, то при наличии в БД записи со значением John Doe она будет найдена, несмотря на раличие в регистре символа. Это произойдет потому, что ORM выполнит SQL с условием вида lower(C.NAME) like ?.

Следует иметь в виду, что при таком поиске индекс, созданный в БД по полю NAME, не используется.

4.4.4.4.2. Макросы в JPQL

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

Макросы, определенные в платформе, решают следующие задачи:

  • Позволяют обойти принципиальную невозможность средствами JPQL выразить условие зависимости значения поля от текущего момента времени (не работает арифметика типа current_date-1)

  • Позволяют сравнивать с датой поля типа Timestamp (содержащие дату+время)

Рассмотрим их подробно:

@between

Имеет вид @between(field_name, moment1, moment2, time_unit), где

  • field_name - имя атрибута для сравнения

  • moment1, moment2 - моменты времени, в которые должно попасть значение атрибута field_name. Каждый из моментов должен быть определен выражением с участием переменной now, к которой может быть прибавлено или отнято целое число

  • time_unit - определяет единицу измерения времени, которое прибавляется или вычитается из now в выражениях моментов, а также точность округления моментов. Может быть следующим: year, month, day, hour, minute, second. При включенном базовом проекте workflow можно также использовать единицы рабочего времени: workday, workhour, workminute.

Макрос преобразуется в следующее выражение JPQL: field_name >= :moment1 and field_name < :moment2

Пример 1. Покупатель создан сегодня:

select c from sales$Customer where @between(c.createTs, now, now+1, day)

Пример 2. Покупатель создан в течение последних 10 минут:

select c from sales$Customer where @between(c.createTs, now-10, now, minute)

Пример 3. Документы, датированные последними 5 рабочими днями (для проектов, включающих workflow):

select d from sales$Doc where @between(d.createTs, now-5, now, workday)
@today

Имеет вид @today(field_name) и обеспечивает формирование условия попадания значения атрибута в текущий день. По сути это частный случай макроса @between.

Пример. Пользователь создан сегодня:

select d from sales$Doc where @today(d.createTs)
@dateEquals

Имеет вид @dateEquals(field_name, parameter) и позволяет сформировать условие попадания значения поля field_name типа Timestamp в дату, задаваемую параметром parameter.

Пример:

select d from sales$Doc where @dateEquals(d.createTs, :param)
@dateBefore

Имеет вид @dateBefore(field_name, parameter) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp меньше даты, задаваемой параметром parameter.

Пример:

select d from sales$Doc where @dateBefore(d.createTs, :param)
@dateAfter

Имеет вид @dateAfter(field_name, parameter) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp больше или равна дате, задаваемой параметром parameter.

Пример:

select d from sales$Doc where @dateAfter(d.createTs, :param)
@enum

Позволяет использовать полное имя константы enum вместо ее идентификатора в БД. Это упрощает поиск использований enum в коде приложения.

Пример:

select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name

Список макросов может быть расширен в прикладном проекте. Для создания нового макроса необходимо определить бин, реализующий интерфейс QueryMacroHandler, и задать ему @Scope("prototype"). Механизм выполнения JPQL запросов создает все доступные бины типа QueryMacroHandler, и по очереди передает им текст запроса с набором параметров. Очередность вызова обработчиков не определена.

4.4.4.5. Выполнение SQL запросов

ORM позволяет выполнять SQL запросы к базе данных, возвращая как списки отдельных полей, так и экземпляры сущностей. Для этого необходимо создать объект Query или TypedQuery вызовом одного из методов EntityManager.createNativeQuery().

Если выполняется выборка отдельных колонок таблицы, то результирующий список будет содержать строки в виде Object[]. Например:

Query query = em.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];
    String name = (String) row[1];
}

Следует иметь в виду, при использовании SQL колонки, соответствующие атрибутам сущностей типа UUID, возвращаются в виде UUID или в виде String, в зависимости от используемой СУБД и JDBC драйвера:

  • HSQLDB - String

  • PostgreSQL, драйвер postgresql-8.3-603.jdbc4.jar - String

  • PostgreSQL, драйвер postgresql-9.1-901.jdbc4.jar - UUID

  • Microsoft SQL Server, драйвер jtds-1.2.4.jar - String

  • Oracle - String

Параметры этого типа также должны задаваться либо как UUID, либо своим строковым представлением, в зависимости от используемой СУБД и JDBC драйвера. Для обеспечения независимости кода от используемой СУБД рекомендуется использовать DbTypeConverter .

Если вместе с текстом запроса передан класс результирующей сущности, то возвращается TypedQuery и после выполнения производится попытка отображения результатов запроса на атрибуты сущности. Например:

TypedQuery<Customer> query = em.createNativeQuery(
    "select * from SALES_CUSTOMER where NAME like ?1",
    Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();

Поведение SQL запросов, возвращающих сущности, и модифицирующих запросов (update, delete), по отношению к текущему персистентному контексту аналогично описанному для JPQL запросов.

См. также Раздел 4.7.10, «Выполнение SQL с помощью QueryRunner».

4.4.4.6. Entity Listeners

Entity Listeners предназначены для реакции на события жизненного цикла экземпляров сущностей на уровне Middleware.

Слушатель представляет собой класс, реализующий один или несколько интерфейсов пакета com.haulmont.cuba.core.listener. Слушатель будет реагировать на события типов, соответствующих реализуемым интерфейсам.

BeforeDetachEntityListener

Метод onBeforeDetach() вызывается перед отделением объекта от EntityManager при коммите транзакции.

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

BeforeAttachEntityListener

Метод onBeforeAttach() вызывается перед введением объекта в персистентный контекст при выполнении операции EntityManager.merge().

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

BeforeInsertEntityListener

Метод onBeforeInsert() вызывается перед выполнением вставки записи в БД. В данном методе возможны любые операции с текущим EntityManager.

AfterInsertEntityListener

Метод onAfterInsert() вызывается после выполнения вставки записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

BeforeUpdateEntityListener

Метод onBeforeUpdate() вызывается перед изменением записи в БД. В данном методе возможны любые операции с текущим EntityManager.

AfterUpdateEntityListener

Метод onAfterUpdate() вызывается после изменения записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

BeforeDeleteEntityListener

Метод onBeforeDelete() вызывается перед удалением записи из БД (или в случае мягкого удаления - перед изменением записи). В данном методе возможны любые операции с текущим EntityManager.

AfterDeleteEntityListener

Метод onAfterDelete() вызывается после удаления записи из БД (или в случае мягкого удаления - после изменения записи), но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

Entity Listener может быть как обычным классом Java, так и управляемым бином. В последнем случае в нем можно использовать инжекцию:

@ManagedBean("cuba_MyEntityListener")
public class MyEntityListener implements
        BeforeInsertEntityListener<MyEntity>,
        BeforeUpdateEntityListener<MyEntity> {

    @Inject
    protected Persistence persistence;

    @Override
    public void onBeforeInsert(MyEntity entity) {
        EntityManager em = persistence.getEntityManager();
        ...
    }

    @Override
    public void onBeforeUpdate(MyEntity entity) {
        EntityManager em = persistence.getEntityManager();
        ...
    }
}

Entity Listener может быть задан 2-мя способами:

  • Статически - имена классов слушателей, или, если слушатель является бином, имена бинов, указываются в аннотации @Listeners на классе сущности:

    @Entity(...)
    @Table(...)
    @Listeners("cuba_MyEntityListener")
    public class MyEntity extends StandardEntity {
        ...
    }
  • Динамически - класс сущности и класс слушателя, или, если слушатель является бином, имя бина, передаются в метод addListener() бина EntityListenerManager. Пример динамического добавления слушателя рассматривается в разделе рецептов разработки: Раздел 5.8.4, «Выполнение кода на старте приложения».

Для всех экземпляров некоторого класса сущности извлекается из контекста Spring или создается и кэшируется один экземпляр слушателя определенного типа, поэтому слушатель не должен иметь состояния.

Если для сущности объявлены несколько слушателей одного типа (например, аннотациями класса сущности и его предков, плюс динамически), то их вызов будет выполняться в следующем порядке:

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

  2. После всех предков вызываются динамически добавленные слушатели данного класса, затем статически назначенные.

4.4.5. Управление транзакциями

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

4.4.5.1. Программное управление транзакциями

Программное управление транзакциями осуществляется с помощью интерфейса com.haulmont.cuba.core.Transaction, ссылку на который можно получить методами createTransaction() или getTransaction() интерфейса инфраструктуры Persistence .

Метод createTransaction() создает новую транзакцию и возвращает интерфейс Transaction. Последующие вызовы методов commit(), commitRetaining(), end() этого интерфейса управляют созданной транзакцией. Если в момент создания существовала другая транзакция, то она будет приостановлена, и возобновлена после завершения созданной.

Метод getTransaction() вызывает либо создание новой, либо присоединение к текущей транзакции. Если в момент вызова существовала активная транзакция, то метод успешно завершается, и последующие вызовы commit(), commitRetaining(), end() не оказывают никакого влияния на существующую транзакцию. Однако если end() вызван без предварительного вызова commit(), то текущая транзакция помечается как RollbackOnly.

Пример ручного управления транзакцией:

@Inject
private Persistence persistence;
...
Transaction tx = persistence.createTransaction();
try {
    EntityManager em = persistence.getEntityManager();
    Customer customer = new Customer();
    customer.setName("John Smith");
    em.persist(customer);

    tx.commit();
} finally {
    tx.end();
}

Интерфейс Transaction имеет также метод execute(), принимающий на вход класс-действие, которое нужно выполнить в данной транзакции. Это позволяет организовать управление транзакциями в функциональном стиле, например:

persistence.createTransaction().execute(new Transaction.Runnable() {
    public void run(EntityManager em) {
        // transactional code here
    }
});

Если транзакционный блок должен вернуть результат, класс-действие должен реализовывать интерфейс Transaction.Callable. Если результат не требуется, как в приведенном примере, то класс-действие удобно наследовать от абстрактного класса Transaction.Runnable.

Следует иметь в виду, что метод execute() у некоторого экземпляра Transaction можно вызвать только один раз, так как после выполнения кода класса-действия транзакция завершается.

4.4.5.2. Декларативное управление транзакциями

Любой метод управляемого бина Middleware можно пометить аннотацией @org.springframework.transaction.annotation.Transactional, что вызовет автоматическое создание транзакции при вызове этого метода. В таком методе не нужно вызывать Persistence.createTransaction(), а можно сразу получать EntityManager и работать с ним.

Для аннотации @Transactional можно указать параметры. Основным параметром является режим создания транзакции - Propagation. Значение REQUIRED соответствует getTransaction(), значение REQUIRES_NEW - createTransaction(). По умолчанию REQUIRED.

Декларативное управление транзакциями позволяет уменьшить количество boilerplate кода, однако имеет следующий недостаток: коммит транзакции происходит вне прикладного кода, что часто затрудняет отладку, т.к. скрывается момент отправки изменений в БД и перехода сущностей в состояние Detached. Кроме того, следует иметь в виду, что декларативная разметка сработает только в случае вызова метода контейнером, т.е. вызов транзакционного метода из другого метода того же самого объекта не приведет к старту транзакции.

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

4.4.5.3. Примеры взаимодействия транзакций

4.4.5.3.1. Откат вложенной транзакции

Если вложенная транзакция создана через getTransaction(), то ее откат приведет к невозможности коммита охватывающей транзакции. Например:

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        // (1) вызываем метод, создающий вложенную транзакцию
        methodB();

        // (4) в этот момент будет выброшено исключение, т.к. транзакция
        //     помечена как rollback only
        tx.commit();
    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        // (2) допустим здесь возникло исключение
        tx.commit();
    } catch (Exception e) {
        // (3) обрабатываем его и выходим
        return;
    } finally {
        tx.end();
    }
}

Если же транзакция в methodB() будет создана через createTransaction(), то ее откат не окажет никакого влияния на коммит охватывающей транзакции в methodA().

4.4.5.3.2. Чтение и изменение данных во вложенной транзакции

Рассмотрим сначала зависимую вложенную транзакцию, создаваемую через getTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        // (1) загружаем сущность, в которой name == "old name"
        Employee employee = em.find(Employee.class, id);
        assertEquals("old name", employee.getName());

        // (2) присваиваем новое значение полю
        employee.setName("name A");

        // (3) вызываем метод, создающий вложенную транзакцию
        methodB();

        // (8) здесь происходит коммит изменений в БД, и в ней
        //     окажется значение "name B"
        tx.commit();

    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        // (4) получаем тот же экземпляр EntityManager, что и methodA
        EntityManager em = persistence.getEntityManager();

        // (5) загружаем сущность с тем же идентификатором
        Employee employee = em.find(Employee.class, id);

        // (6) значение поля новое, т.к. мы работаем с тем же
        //     персистентным контекстом, и обращения к БД вообще
        //     не происходит
        assertEquals("name A", employee.getName());
        employee.setName("name B");

        // (7) в этот момент реально коммита не происходит
        tx.commit();
    } finally {
        tx.end();
    }
}

Теперь рассмотрим тот же самый пример с независимой вложенной транзакцией, создаваемой через createTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        // (1) загружаем сущность, в которой name == "old name"
        Employee employee = em.find(Employee.class, id);
        assertEquals("old name", employee.getName());

        // (2) присваиваем новое значение полю
        employee.setName("name A");

        // (3) вызываем метод, создающий вложенную транзакцию
        methodB();

        // (8) здесь возникнет исключение из-за оптимистичной блокировки
        //     и коммит не пройдет вообще
        tx.commit();

    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.createTransaction();
    try {
        // (4) создается новый экземпляр EntityManager, т.к. это
        //     новая транзакция
        EntityManager em = persistence.getEntityManager();

        // (5) загружаем сущность с тем же идентификатором
        Employee employee = em.find(Employee.class, id);

        // (6) значение поля старое, т.к. произошла загрузка из БД
        //     старого экземпляра сущности
        assertEquals("old name", employee.getName());

        employee.setName("name B");

        // (7) здесь происходит коммит изменений в БД, и в ней
        //     окажется значение "name B"
        tx.commit();

    } finally {
        tx.end();
    }
}

В последнем случае исключение в точке (8) возникнет, только если сущность является оптимистично блокируемой, т.е. если она реализует интерфейс Versioned.

4.4.5.4. Таймаут транзакции

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

При программном управлении транзакциями таймаут включается путем передачи объекта TransactionParams в метод Persistence.createTransaction(). Например:

Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));

При декларативном управлении транзакциями используется параметр timeout аннотации @Transactional, например:

@Transactional(timeout = 2)
public void someServiceMethod() {
...

Таймаут по умолчанию может быть задан в свойстве приложения cuba.defaultQueryTimeoutSec .

4.4.5.4.1. Особенности реализации для различных СУБД

PostgreSQL

К сожалению, JDBC драйвер PostgreSQL не поддерживает метод setQueryTimeout() интерфейса java.sql.Statement, поэтому в начале каждой транзакции, для которой определен таймаут (любым способом, включая ненулевое значение свойства cuba.defaultQueryTimeoutSec ), выполняется дополнительный оператор в БД: set local statement_timeout to {value}. При этом в случае превышения таймаута запрос будет прерван самим сервером БД.

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

  • Таймаут по умолчанию устанавливать не на Middleware с помощью свойства cuba.defaultQueryTimeoutSec, а на самом сервере PostgreSQL в файле postgresql.conf, например, statement_timeout = 3000 (это в миллисекундах).

  • Для методов, которым требуется большее время таймаута (отчеты и пр.), явно указывать желаемый таймаут в параметрах транзакции.

Microsoft SQL Server

Драйвер JTDS поддерживает метод setQueryTimeout() интерфейса java.sql.Statement, поэтому для EntityManager просто устанавливается стандартное свойство javax.persistence.query.timeout, которое соответствующим образом влияет на JDBC запросы.

4.5. Универсальный пользовательский интерфейс

Подсистема универсального пользовательского интерфейса (Generic UI, GUI) позволяет разрабатывать экраны пользовательского интерфейса, используя XML и Java. Созданные таким образом экраны одинаково работоспособны в двух стандартных клиентских блоках: Web Client и Desktop Client.

Рисунок 15. Структура универсального пользовательского интерфейса

Структура универсального пользовательского интерфейса

Здесь в центре изображены основные составляющие экранов универсального пользовательского интерфейса:

  • XML-дескрипторы - файлы XML, содержащие информацию об источниках данных и компоновке экрана

  • Контроллеры - классы Java, содержащие логику инициализации экрана и обработки событий от элементов пользовательского интерфейса.

Код экранов приложения, расположенный в модуле gui, взаимодействует с интерфейсами визуальных компонентов (VCL Interfaces), реализованными по-отдельности в модулях web и desktop базового проекта cuba. Для Web Client реализация основана на фреймворке Vaadin, для Desktop Client – на фреймворке Java Swing.

Библиотека визуальных компонентов (Visual Components Library, VCL) содержит большой набор готовых компонентов для отображения данных.

Механизм источников данных (Datasources) предоставляет унифицированный интерфейс, обеспечивающий функционирование связанных с данными визуальных компонентов.

Инфраструктура клиента (Infrastructure) включает в себя главное окно приложения, механизмы отображения и взаимодействия экранов UI, а также средства взаимодействия со средним слоем.

4.5.1. Экраны

Экран универсального пользовательского интерфейса состоит из XML-дескриптора и класса контроллера. Дескриптор содержит ссылку на класс контроллера.

Для того чтобы экран можно было вызывать из главного меню или из Java кода (например, из контроллера другого экрана), XML-дескриптор должен быть зарегистрирован в файле screens.xml проекта.

Главное меню приложения формируется отдельно для Web Client и Desktop Client на основе файлов menu.xml , расположенных соответственно в модулях web и desktop проекта.

4.5.1.1. Типы экранов

В данном разделе рассматриваются основные типы экранов:

4.5.1.1.1. Фрейм

Фреймы представляют собой части экранов, которые применяются для декомпозиции и многократного использования.

Для подключения фрейма в XML экрана используется элемент iframe c указанием либо пути к файлу XML фрейма, либо идентификатора фрейма, если он зарегистрирован в screens.xml .

Контроллер фрейма должен быть унаследован от класса AbstractFrame.

Правила взаимодействия экрана и вложенного в него фрейма:

  • Из экрана обращаться к компонентам фрейма можно через точку: frame_id.component_id

  • Из контроллера фрейма получить компонент экрана можно обычным вызовом getComponent(component_id), но только в том случае, если компонент с таким именем не объявлен в самом фрейме. То есть компоненты фрейма маскируют компоненты экрана.

  • Из фрейма получить источник данных экрана можно простым вызовом getDsContext().get(ds_id) или инжекцией, либо в запросе ds$ds_id, но только в том случае, если источник данных с таким именем не объявлен в самом фрейме (аналогично компонентам).

  • Из экрана получить источник данных фрейма можно только через итерацию по getDsContext().getChildren()

При коммите экрана вызывается также коммит измененных источников данных фрейма.

4.5.1.1.2. Простой экран

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

Идентификатор экрана в файле screens.xml может быть произвольного вида.

Контроллер простого экрана должен быть унаследован от класса AbstractWindow.

4.5.1.1.3. Экран выбора

Экран выбора (lookup) отличается от простого экрана тем, что при вызове методом openLookup() отображает внизу панель с кнопками, позволяющими передать вызывающему коду экземпляр выбранной в данный момент в списке сущности. При вызове методом openWindow() или, например, из главного меню, панель с кнопками выбора не отображается.

В метод openLookup() передается объект с интерфейсом Window.Lookup.Handler. Метод handleLookup() этого объекта вызывается экраном, и ему передается коллекция выбранных пользователем экземпляров сущности. Тем самым вызывающий код получает из экрана выбранные экземпляры.

Экраны выбора рекомендуется использовать для отображения списков сущностей. Визуальные компоненты, предназначенные для отображения и редактирования ссылок между сущностями (такие как PickerField , LookupPickerField , SearchPickerField ), вызывают экраны выбора для поиска связанных сущностей.

Для корректной работы стандартных действий идентификатор экрана выбора в файле screens.xml должен иметь вид {имя_сущности}.lookup, например, sales$Customer.lookup.

Контроллер экрана выбора должен быть унаследован от класса AbstractLookup. В XML экрана в атрибуте lookupComponent должен быть указан компонент (например, Table ), из которого будет взят экземпляр сущности при выборе.

4.5.1.1.4. Экран редактирования

Экран редактирования предназначен для отображения и редактирования экземпляра сущности. Поддерживает функциональность установки редактируемого экземпляра и действия по коммиту изменений в базу данных. Экран редактирования должен вызываться методом openEditor() с передачей экземпляра сущности.

Для корректной работы стандартных действий идентификатор экрана редактирования в файле screens.xml должен иметь вид {имя_сущности}.edit, например, sales$Customer.edit.

Контроллер экрана редактирования должен быть унаследован от класса AbstractEditor. В XML экрана в атрибуте datasource указывается источник данных, в который проставляется редактируемый экземпляр сущности. Для отображения действий, выполняющих коммит или отмену изменений, в XML можно использовать следующие стандартные фреймы с кнопками:

  • editWindowActions (файл com/haulmont/cuba/gui/edit-window.actions.xml) - содержит кнопки OK и Cancel

  • extendedEditWindowActions (файл com/haulmont/cuba/gui/extended-edit-window.actions.xml) - содержит кнопки OK & Close, OK и Cancel

В экране редактирования неявно создаются следующие действия:

  • windowCommitAndClose (соответствует константе Window.Editor.WINDOW_COMMIT_AND_CLOSE) - действие, выполняющее коммит изменений в базу данных и закрывающее экран. Создается при наличии в экране визуального компонента с идентификатором windowCommitAndClose, в частности, при использовании вышеописанного стандартного фрейма extendedEditWindowActions отображается кнопкой OK & Close.

  • windowCommit (соответствует константе Window.Editor.WINDOW_COMMIT) - действие, выполняющее коммит изменений в базу данных. При отсутствии действия windowCommitAndClose после коммита закрывает экран. Создается всегда, и при наличии в экране вышеописанных стандартных фреймов отображается кнопкой OK.

  • windowClose (соответствует константе Window.Editor.WINDOW_CLOSE) - действие, закрывающее экран без коммита изменений. Создается всегда, и при наличии в экране вышеописанных стандартных фреймов отображается кнопкой Cancel.

Таким образом, если в экран добавлен фрейм editWindowActions, то кнопка OK коммитит изменения и закрывает экран, а кнопка Cancel - закрывает без коммита. Если же добавлен фрейм extendedEditWindowActions, то кнопка OK только коммитит изменения, оставляя экран открытым, кнопка OK & Close коммитит и закрывает экран, кнопка Cancel - закрывает без коммита.

Вместо стандартных фреймов для отображения действий можно использовать произвольные компоненты, например, LinkButton .

4.5.1.2. XML-дескриптор

XML-дескриптор - это файл формата XML, описывающий источники данных и расположение визуальных компонентов экрана.

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/window.xsd

Рассмотрим структуру дескриптора.

window − корневой элемент.

Атрибуты window:

  • class − имя класса контроллера

  • messagesPackпакет сообщений данного экрана, который будет использован при получении локализованных строк без указания пакета из XML-дескриптора и из контроллера методом getMessage()

  • caption − заголовок экрана, может содержать ссылку на сообщение из вышеуказанного пакета, например,

    caption="msg://caption"
  • focusComponent − идентификатор компонента, который получит фокус ввода при отображении экрана

  • lookupComponent - обязательный для экрана выбора атрибут, задающий идентификатор визуального компонента, из которого будет выбран экземпляр сущности. Поддерживаются компоненты следующих типов (и их наследников):

    • Table

    • Tree

    • LookupField

    • PickerField

    • OptionsGroup

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

Элементы window:

  • metadataContext − элемент для инициализации представлений (views), необходимых данному экрану. Предпочтительным является определение всех представлений в одном общем файле views.xml , так как все описатели представлений разворачиваются в один общий репозиторий, и при рассредоточении описателей по разным файлам трудно обеспечить уникальность имен.

  • dsContext − определяет источники данных данного экрана.

  • actions - определяет список действий данного экрана.

  • timers - определяет список таймеров данного экрана.

  • companions - определяет список классов-компаньонов данного контроллера

    Элементы companions:

    • web - задает компаньон, реализованный в модуле web

    • desktop - задает компаньон, реализованный в модуле desktop

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

  • layout − корневой элемент компоновки экрана. Является сам по себе контейнером с вертикальным расположением компонентов, аналогичным vbox .

    Атрибуты layout:

4.5.1.3. Контроллер экрана

Контроллер экрана - это Java или Groovy класс, связанный с XML-дескриптором, и содержащий логику инициализации и обработки событий экрана.

Контроллер должен быть унаследован от одного из следующих базовых классов:

Если экрану не нужна никакая дополнительная логика, то в качестве контроллера можно использовать сам базовый класс AbstractWindow, AbstractLookup или AbstractEditor, указав его в XML-дескрипторе (эти классы на самом деле не являются абстрактными в смысле невозможности создания экземпляров). Для фрейма класс контроллера можно не указывать вообще.

Класс контроллера должен быть зарегистрирован в XML-дескрипторе экрана в атрибуте class корневого элемента window.

Рисунок 16. Базовые классы контроллеров

Базовые классы контроллеров

4.5.1.3.1. AbstractFrame

AbstractFrame является корнем иерархии классов контроллеров. Рассмотрим его основные методы:

  • init() - вызывается фреймворком после создания всего дерева компонентов, описанного XML-дескриптором, но до отображения экрана.

    В метод init() из вызывающего кода передается мэп параметров, которые могут быть использованы внутри контроллера. Эти параметры могут быть переданы как из кода контроллера вызывающего экрана (в методе openWindow(), openLookup() или openEditor()), так и установлены в файле регистрации экранов screens.xml .

    Метод init() следует имплементировать при необходимости инициализации компонентов экрана, например:

    @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) {
              ...
          }
      });
    }
  • getMessage(), formatMessage() - методы получения локализованных сообщений из пакета, заданного для экрана в XML-дескрипторе. Представляют собой просто короткие варианты вызова одноименных методов интерфейса Messages .

  • getDialogParams() - получить объект DialogParams для установки параметров отображения диалоговых окон (высота, ширина и пр.). Значения, установленные в этом объекте, влияют на следующий экран, открываемый в режиме модального диалога (WindowManager.OpenType.DIALOG). После отображения диалога они сбрасываются в значения по умолчанию.

    Таким образом, устанавливать значения в DialogParams необходимо непосредственно перед вызовом другого экрана в режиме диалога методами openWindow(), openLookup(), openEditor(). Например:

    getDialogParams().setWidth(400);
    openEditor("sales$Customer.edit", customer, WindowManager.OpenType.DIALOG);

    Если же сам текущий экран открывается в режиме модального диалога, то можно управлять параметрами его отображения, устанавливая параметры DialogParams в его методе init(). При этом установленные в init() параметры имеют приоритет над установленными в вызывающем коде.

  • openFrame() - загрузить фрейм по идентификатору, зарегистрированному в screens.xml , и, если в метод передан компонент-контейнер, отобразить его внутри контейнера. Возвращается контроллер фрейма. Например:

    @Inject
    private BoxLayout container;
    
    @Override
    public void init(Map<String, Object> params) {
      SomeFrame frame = openFrame(container, "someFrame");
      frame.setHeight("100%");
      frame.someInitMethod();
    }

    Контейнер не обязательно сразу передавать в метод openFrame(), вместо этого можно загрузить фрейм, а затем добавить его в нужный контейнер:

    @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);
    }
  • openWindow(), openLookup(), openEditor() - открыть соответственно простой экран, экран выбора или редактирования. Методы возвращают контроллер созданного экрана.

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

    CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB);
    editor.addListener(new CloseListener() {
      @Override
      public void windowClosed(String actionId) {
          // do something
      }
    });
  • showMessageDialog() - отобразить диалоговое окно с сообщением.

  • showOptionDialog() - отобразить диалоговое окно с сообщением и возможностью выбора пользователем некоторых действий. Действия задаются массивом объектов типа Action , которые в диалоге отображаются посредством соответствующих кнопок.

    Для отображения стандартных кнопок типа OK, Cancel и других рекомендуется использовать объекты типа DialogAction, например:

    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);
          });
  • showNotification() - отобразить всплывающее окно с сообщением.

  • showWebPage() - открыть указанную веб-страницу в браузере.

4.5.1.3.2. AbstractWindow

AbstractWindow является наследником AbstractFrame , и определяет следующие собственные методы:

  • ready() - шаблонный метод, который можно имплементировать в контроллере для перехвата момента открытия экрана. Метод ready() вызывается фреймворком после метода init() непосредственно перед показом экрана в главном окне приложения.

  • validateAll() - валидация экрана. Реализация по умолчанию вызывает метод validate() у всех компонентов экрана, реализующих интерфейс Component.Validatable, накапливает информацию об исключениях, и если таковые имеются, выводит соответствующее сообщение и возвращает false, иначе возвращает true.

    Данный метод следует переопределять только в том случае, если необходимо полностью заменить стандартную процедуру валидации экрана. Если же нужно только дополнить ее, достаточно определить специальный шаблонный метод postValidate().

  • postValidate() - шаблонный метод, который можно имплементировать в контроллере для дополнительной валидации экрана. Получаемый методом объект ValidationErrors используется для добавления информации об ошибках валидации, которая будет отображена совместно с ошибками стандартной валидации. Например:

    private Pattern pattern = Pattern.compile("\\d");
    
    @Override
    protected void postValidate(ValidationErrors errors) {
      if (getItem().getAddress().getCity() != null) {
          if (pattern.matcher(getItem().getAddress().getCity()).find()) {
              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, то процесс закрытия экрана прерывается.

4.5.1.3.3. AbstractLookup

AbstractLookup базовый класс контроллеров экранов выбора, является наследником AbstractWindow , и определяет следующие собственные методы:

  • setLookupComponent() - установить компонент, из которого будет производиться выбор экземпляров сущности.

    Как правило, компонент выбора устанавливается в XML-дескрипторе экрана, и вызывать данный метод в прикладном коде нет необходимости.

  • setLookupValidator() - установить для экрана объект типа Window.Lookup.Validator, метод validate() которого вызывается фреймворком перед тем как вернуть выбранные экземпляры сущностей. Если validate() возвращает false, процесс выбора и закрытия экрана прерывается.

    По умолчанию валидатор не установлен.

4.5.1.3.4. AbstractEditor

AbstractEditor − базовый класс контроллеров экранов редактирования, является наследником AbstractWindow .

При создании конкретного класса контроллера рекомендуется параметризовать AbstractEditor типом редактируемой сущности. При этом методы getItem() и initItem() будут работать с конкретным типом сущности и прикладному коду не потребуется дополнительных приведений типов. Например:

public class CustomerEdit extends AbstractEditor<Customer> {

  @Override
  protected void initItem(Customer item) {
  ...

AbstractEditor определяет следующие собственные методы:

  • getItem() - возвращает экземпляр редактируемой сущности, установленный в главном источнике данных экрана (т.е. указанном в атрибуте datasource корневого элемента XML-дескриптора).

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

    Изменения, вносимые в экземпляр, возвращаемый getItem(), отражаются на состоянии источника данных, и будут отправлены на Middleware при коммите экрана.

    Следует иметь в виду, что getItem() возвращает значение только после инициализации экрана методом setItem(). До этого момента, например, в методах init() и initItem(), данный метод возвращает null.

    Однако в методе init() экземпляр сущности, переданный в openEditor(), можно получить из параметров следующим образом:

    @Override
    public void init(Map<String, Object> params) {
      Customer item = WindowParams.ITEM.getEntity(params);
      // do something
    }

    В метод initItem() экземпляр передается явно и нужного типа.

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

  • setItem() - вызывается фреймворком при открытии экрана методом openEditor() для установки редактируемого экземпляра сущности в главном источнике данных. В момент вызова созданы все компоненты и источники данных экрана, и отработал метод init() контроллера.

    Для инициализации экрана редактирования вместо переопределения setItem() рекомендуется имплементировать специальные шаблонные методы initItem() и 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(), который будет возвращать новый или перезагруженный при инициализации экрана экземпляр сущности.

    Данный метод можно имплементировать в контроллере для окончательной инициализации экрана, например:

    @Inject
    protected EntityDiffViewer diffFrame;
    
    @Override
    protected void postInit() {
      if (!PersistenceHelper.isNew(getItem())) {
          diffFrame.loadVersions(getItem());
      }
    }
  • commit() - валидировать экран и отправить изменения через DataSupplier на Middleware.

    Если используется вариант метода с параметром validate = false, то валидация перед коммитом не производится.

    Данный метод не рекомендуется переопределять, лучше использовать специальные шаблонные методы postValidate(), preCommit() и postCommit().

  • commitAndClose() - валидировать экран, отправить изменения на Middleware и закрыть экран. В метод preClose() и зарегистрированным слушателям CloseListener будет передано значение константы Window.COMMIT_ACTION_ID.

    Данный метод не рекомендуется переопределять, лучше использовать специальные шаблонные методы postValidate(), preCommit() и postCommit().

  • preCommit() - шаблонный метод, вызываемый фреймворком в процессе коммита изменений, после того как валидация завершена успешно и перед отправкой данных на Middleware.

    Данный метод можно имплементировать в контроллере. Если метод возвращает false, процесс коммита (и закрытия экрана, если был вызван commitAndClose()), прерывается. Например:

    @Override
    protected boolean preCommit() {
      if (somethingWentWrong) {
          showNotification("Something went wrong", NotificationType.WARNING);
          return false;
      }
      return true;
    }
  • postCommit() - шаблонный метод, вызываемый фреймворком на финальной стадии коммита изменений. Параметры метода:

    • committed - установлен в true, если в экране действительно были изменения, и они отправлены на Middleware;

    • 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.getItem(), driverDs.getView());
      }
      return super.postCommit(committed, close);
    }

Далее приведены диаграммы последовательностей инициализации и различных вариантов коммита экрана редактирования.

Рисунок 17. Инициализация экрана редактирования

Инициализация экрана редактирования

Рисунок 18. Коммит и закрытие экрана с фреймом editWindowActions

Коммит и закрытие экрана с фреймом editWindowActions

Рисунок 19. Коммит экрана с фреймом extendedEditWindowActions

Коммит экрана с фреймом extendedEditWindowActions

Рисунок 20. Коммит и закрытие экрана с фреймом extendedEditWindowActions

Коммит и закрытие экрана с фреймом extendedEditWindowActions

4.5.1.3.5. Инжекция зависимостей контроллеров

В контроллерах можно использовать Dependency Injection для получения ссылок на используемые объекты. Для этого нужно объявить либо поле соответствующего типа, либо метод доступа на запись (setter) с соответствующим типом результата, и добавить ему одну из следующих аннотаций:

  • @Inject - простейший вариант, поиск объекта для инжекции будет произведен по типу поля/метода и по имени, эквивалентному имени поля либо имени атрибута (по правилам JavaBeans) для метода

  • @Named("someName") - вариант с явным указанием имени искомого объекта

Инжектировать в контроллеры можно следующие объекты:

  • Визуальные компоненты данного экрана, определенные в XML-дескрипторе. Если тип атрибута унаследован от Component, в текущем экране будет произведен поиск компонента с соответствующим именем.

  • Действия, определенные в XML-дескрипторе - см. Раздел 4.5.4, «Действия. Интерфейс Action»

  • Источники данных, определенные в XML-дескрипторе. Если тип атрибута унаследован от Datasource, в текущем экране будет произведен поиск источника данных с соответствующим именем.

  • UserSession. Если тип атрибута - UserSession , будет инжектирован объект текущей пользовательской сессии.

  • DsContext. Если тип атрибута - DsContext, будет инжектирован DsContext текущего экрана.

  • WindowContext. Если тип атрибута - WindowContext, будет инжектирован WindowContext текущего экрана.

  • DataSupplier. Если тип атрибута - DataSupplier , будет инжектирован соответствующий экземпляр.

  • Любой бин, определенный в контексте данного клиентского блока приложения, в том числе:

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

С помощью специальной аннотации @WindowParam можно инжектировать в контроллер параметры, передаваемые в мэп метода init(). Аннотация имеет атрибут name, в котором указывается имя параметра (ключ в мэп), и опциональный атрибут required. Если required = true, то при отсутствии в мэп соответствующего параметра в лог выводится сообщение с уровнем WARNING.

Пример инжекции объекта типа Job, передаваемого в метод init() контроллера:

@WindowParam(name = "job", required = true)
protected Job job;
4.5.1.3.6. Компаньоны контроллеров

Базовые классы контроллеров расположены в модуле gui базового проекта cuba и не содержат ссылок на классы реализации визуальных компонентов (Swing или Vaadin), что дает возможность использовать их в клиентах обоих типов. Вместо этого базовые классы контроллеров реализуют дополнительный интерфейс Window.Wrapper и делегируют выполнение "обернутому" окну.

В то же время конкретные классы контроллеров могут быть расположены как в модуле gui, так и в web или desktop, в зависимости от применяемых в проекте клиентских блоков и специфики экрана. Если контроллер является универсальным, но для разных типов клиента требуется дополнительная функциональность, ее можно определить в так называемых классах-компаньонах.

Класс-компаньон располагается в модуле клиента соответствующего типа (web или desktop) и реализует интерфейс, задаваемый в использующем его контроллере. Класс компаньона задается в элементе companions XML-дескриптора экрана. Контроллер может получить ссылку на экземпляр компаньона с помощью инжекции или вызовом getCompanion(), и в нужный момент передать ему управление, например, для дополнительной инициализации визуальных компонентов специфичным для данного типа клиента способом.

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

public class CustomerBrowse extends AbstractLookup {

  public interface Companion {
      void initTable(Table table);
  }

  @Inject
  protected Table table;

  @Inject
  protected Companion companion;

  @Override
  public void init(Map<String, Object> params) {
      if (companion != null) {
          companion.initTable(table);
      }
  }
}

В модулях web и desktop создаем соответствующие классы реализации компаньона:

public class WebCustomerBrowseCompanion implements CustomerBrowse.Companion {
  @Override
  public void initTable(Table 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 table) {
      javax.swing.JTable desktopTable = (javax.swing.JTable) DesktopComponentsHelper.unwrap(table);
      // do something specific to Swing table
  }
}

И регистрируем классы реализации компаньона в XML-дескрипторе экрана:

<window ...
      class="com.company.sample.gui.customers.CustomerBrowse">
  <companions>
      <web class="com.company.sample.web.customers.WebCustomerBrowseCompanion"/>
      <desktop class="com.company.sample.desktop.customers.DesktopCustomerBrowseCompanion"/>
  </companions>
  <dsContext>...</dsContext>
  <layout>...</layout>
</window>

Так как классы-компаньоны расположены в web и desktop модулях, в них можно использовать метод unwrap() классов WebComponentsHelper и DesktopComponentsHelper для извлечения из интерфейса Table ссылок на реализующие таблицу Vaadin и Swing компоненты, и работать с ними непосредственно.

4.5.2. Библиотека визуальных компонентов

Компоненты

Контейнеры

Разное

4.5.2.1. Компоненты

Рисунок 21. Диаграмма компонентов

Диаграмма компонентов

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

4.5.2.1.1. Button

Кнопка (Button) − компонент, обеспечивающий выполнение действия при нажатии.

XML-имя компонента: button

Компонент кнопки реализован для блоков Web Client и Desktop Client.

Кнопка может содержать текст или пиктограмму (или и то и другое). На рисунке ниже отображены разные виды кнопок.

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

<button id="textButton" caption="msg://someAction" description="Press me"/>

Название кнопки задается с помощью атрибута caption, всплывающая подсказка − с помощью атрибута description.

Атрибут icon указывает на местоположение пиктограммы. Подробную информацию о том, где следует располагать файлы пиктограмм, можно прочитать в Раздел 4.5.7, «Создание темы приложения»

Пример создания кнопки с пиктограммой:

<button id="iconButton" caption="" icon="icons/save.png"/>

Основная функция кнопки − выполнить некоторое действие при нажатии на нее. Определить метод контроллера, который будет вызываться при нажатии на кнопку, можно с помощью атрибута invoke. Значением атрибута должно быть имя метода контроллера, удовлетворяющего следующим условиям:

  • Метод должен быть public.

  • Метод должен возвращать void.

  • Метод должен либо не иметь аргументов, либо иметь один аргумент типа Component. Если метод имеет аргумент Component, то при вызове в него будет передан экземпляр вызвавшей кнопки.

В качестве примера показано описание кнопки, вызывающей метод someMethod:

<button invoke="someMethod" caption="msg://someButton"/>

В контроллере экрана необходимо определить метод someMethod:

public void someMethod() {
//some actions
}

Атрибут invoke игнорируется, если для кнопки задан атрибут action. Атрибут action содержит имя действия, соответствующего данной кнопке.

Пример кнопки с атрибутом action:

<actions>
<action id="someAction" caption="msg://someAction"/>
</actions>
<layout>
<button action="someAction"/>

Кнопке можно назначить любое действие, имеющееся в каком-либо компоненте, реализующем интерфейс Component.ActionsHolder (это актуально для Table, GroupTable, TreeTable, Tree). Причем неважно, каким образом эти действия добавлены - декларативно в XML-дескрипторе или программно в контроллере. В любом случае для использования такого действия достаточно в атрибуте action указать через точку имя компонента и идентификатор нужного действия. Например, в следующем примере кнопке назначается действие create таблицы coloursTable:

<button action="coloursTable.create"/>

Действие для кнопки можно также создавать программно, в контроллере экрана, используя наследование от класса AbstractAction.

Если для Button установлен экземпляр Action, то кнопка возьмет из него следующие свои свойства: caption, description, icon, enable, visible. Свойства caption и description будут проставлены из действия только в том случае, если они не установлены в самом Button. Остальные перечисленные свойства действия имеют безусловный приоритет над свойствами кнопки. Если свойства действия меняются уже после установки этого Action для Button, то соответственно меняться будут и свойства Button, то есть кнопка слушает изменение свойств действия. В этом случае меняется и свойства caption и description, причем даже если они изначально были назначены на саму кнопку.

Атрибуты button:

4.5.2.1.2. Bulk Editor

Bulk Editor - компонент, позволяющий менять значения атрибутов сразу нескольких выбранных экземпляров сущностей. Компонент представляет собой кнопку, добавляющуюся к таблице или дереву и при нажатии открывающую редактор сущностей.

XML-имя компонента: bulkEditor

Компонент реализован для блоков Web Client и Desktop Client.

Для использования Bulk Editor у таблицы или дерева должен быть задан атрибут multiselect="true".

Экран редактирования сущностей генерируется автоматически на основе заданного представления (содержащего только поля данной сущности, в том числе ссылки) и разрешений пользователя. Системные атрибуты в редакторе также не отображаются.

Атрибуты сущности в редакторе сортируются по алфавиту. По умолчанию они пусты. При коммите экрана заданные на экране непустые значения атрибутов проставляются всем выбранным экземплярам сущности.

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

Пример описания компонента bulkEditor для таблицы:

<table id="invoiceTable"
       multiselect="true"
       width="100%">
    <actions>
        <!-- ... -->
    </actions>
    <buttonsPanel>
        <!-- ... -->
        <bulkEditor for="invoiceTable"
                    exclude="customer"/>
    </buttonsPanel>

Атрибут for является обязательным. В нем указывается идентификатор таблицы или дерева, в данном случае - invoiceTable.

Атрибут exclude может содержать регулярное выражения для явного исключения определенных полей из списка редактируемых. Например: date|customer

Атрибуты BulkEditor:

4.5.2.1.3. CheckBox

Флажок (CheckBox) − компонент, имеющий два состояния: выбран, не выбран.

XML-имя компонента: checkBox.

Компонент CheckBox реализован для блоков Web Client и Desktop Client.

Пример флажка с надписью, взятой из пакета локализованных сообщений:

<checkBox id="accessField" caption="msg://accessFieldCaption"/>

Сброс или установка флажка изменяет его значение: Boolean.TRUE или Boolean.FALSE. Значение может быть получено с помощью метода getValue() и установлено с помощью метода setValue(). Если в setValue() передать null, то устанавливается значение Boolean.FALSE и флажок снимается.

Изменение значения флажка, так же как и любого другого компонента, реализующего интерфейс Field, можно отслеживать с помощью слушателя ValueListener. Например:

@Inject
private CheckBox accessField;

@Override
public void init(Map<String, Object> params) {
accessField.addListener(new ValueListener<Object>() {
    @Override
    public void valueChanged(Object source, String property, Object prevValue, Object value) {
        if (Boolean.TRUE.equals(value)) {
            showNotification("set", NotificationType.HUMANIZED);
        } else {
            showNotification("not set", NotificationType.HUMANIZED);
        }
    }
});
}

Для создания флажка, связанного с данными, необходимо использовать атрибуты datasource и property.

<dsContext>
<datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
</dsContext>
<layout>
<checkBox datasource="customerDs" property="active"/>

Как видно из примера, в экране описывается источник данных customerDs для некоторой сущности Покупатель (Customer), имеющей атрибут active. В компоненте checkBox в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено флажком. Атрибут должен быть типа Boolean. Значением атрибута может быть null, при этом флажок снимается.

Атрибуты checkBox:

4.5.2.1.4. DateField

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

XML-имя компонента: dateField.

Компонент DateField реализован для блоков Web Client и Desktop Client.

  • Для создания поля даты, связанного с данными, необходимо использовать атрибуты datasource и property:

    <dsContext>
        <datasource id="orderDs" class="com.sample.sales.entity.Order" view="_local"/>
    </dsContext>
    <layout>
        <dateField datasource="orderDs" property="date"/>

    Как видно из примера, в экране описывается источник данных orderDs для некоторой сущности Заказ (Order), имеющей атрибут date. В компоненте ввода даты в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в поле.

  • Если поле связано с атрибутом сущности, то оно автоматически принимает соответствующий вид:

  • Изменить формат представления даты и времени можно с помощью атрибута dateFormat. Значением атрибута может быть либо сама строка формата, либо ключ в пакете сообщений (если значение начинается с msg://).

    Формат задается по правилам класса SimpleDateFormat (http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html). Если в формате отсутствуют символы H или h, то поле времени не выводится.

    <dateField dateFormat="MM/yy" caption="msg://monthOnlyDateField"/>
  • Если для пользователя методом setTimeZone() задан часовой пояс, то DateField может преобразовывать значения типа timestamp между часовыми поясами сервера и пользователя. Если компонент привязан к атрибуту типа timestamp, часовой пояс автоматически берется из текущей пользовательской сессии. Если нет, то можно вызвать метод setTimeZone() в контроллере экрана, чтобы DateField выполнил необходимые преобразования.

  • Точность представления даты и времени можно определить с помощью атрибута resolution. Значение атрибута должно соответствовать перечислению DateField.ResolutionSEC, MIN, HOUR, DAY, MONTH, YEAR. По умолчанию - MIN, то есть до минут.

    Если resolution="DAY" и не указан атрибут dateFormat, то в качестве формата будет взят формат, указанный в главном пакете сообщений с ключом dateFormat.

    Если resolution="MIN" и не указан атрибут dateFormat, то в качестве формата будет взят формат, указанный в главном пакете сообщений с ключом dateTimeFormat.

    Ниже показано определения поля для ввода даты с точностью до месяца.

    <dateField resolution="MONTH" caption="msg://monthOnlyDateField"/>

.

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

Атрибуты dateField:

Элементы dateField:

4.5.2.1.5. Embedded

Компонент Embedded предназначен для вывода изображений и встраивания в экран произвольных веб-страниц.

XML-имя компонента: embedded

Компонент реализован для блоков Web Client и Desktop Client. В десктоп-клиенте поддерживается только вывод изображений.

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

  • Объявляем компонент в XML-дескрипторе экрана:

    <groupBox caption="Embedded" spacing="true"
          height="250px" width="250px" expand="embedded">
    <embedded id="embedded" width="100%"
              align="MIDDLE_CENTER"/>
    </groupBox>
  • В контроллере экрана инжектируем компонент и интерфейс FileStorageService. Затем в методе init() получаем из параметров экрана переданный из вызывающего кода FileDescriptor, загружаем соответствующий файл в байтовый массив, создаем для него ByteArrayInputStream и передаем в метод setSource() компонента:

    @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 = 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);
    }
    }

Веб-клиент позволяет выводить изображения из произвольных файлов на диске, доступных блоку Web Client. Для этого нужно определить каталог ресурсных файлов в свойстве приложения cuba.web.resourcesRoot, и указать для компонента Embedded имя файла внутри этого каталога:

embedded.setSource("my-logo.png");

Для встраивания в экран веб-клиента внешней веб-страницы необходимо передать компоненту URL:

try {
embedded.setSource(new URL("http://www.cuba-platform.com"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}

Атрибуты embedded:

align id  
height visible  
width   
4.5.2.1.6. FieldGroup

Компонент FieldGroup предназначен для совместного отображения и редактирования нескольких атрибутов сущностей.

XML-имя компонента: fieldGroup

Компонент реализован для блоков Web Client и Desktop Client.

Пример описания группы полей в XML-дескрипторе экрана:

<dsContext>
    <datasource id="orderDs"
                class="com.sample.sales.entity.Order"
                view="orderWithCustomer">
    </datasource>
</dsContext>
<layout>
    <fieldGroup id="orderFieldGroup" datasource="orderDs" width="250px">
        <field id="date"/>
        <field id="customer"/>
        <field id="amount"/>
    </fieldGroup>

Здесь в элементе dsContext определен источник данных datasource, который содержит один экземпляр сущности Order. Для компонента fieldGroup в атрибуте datasource указывается используемый источник данных, а в элементах field - какие атрибуты сущности, содержащейся в источнике данных, необходимо отобразить.

Элементы fieldGroup:

  • column - необязательный элемент, позволяющий располагать поля в несколько колонок. Для этого элементы field должны находиться не непосредственно внутри fieldGroup, а внутри своего column. Например:

    <fieldGroup id="orderFieldGroup" datasource="orderDs" width="100%">
        <column width="250px">
            <field id="num"/>
            <field id="date"/>
            <field id="amount"/>
        </column>
        <column width="400px">
            <field id="customer"/>
            <field id="info"/>
        </column>
    </fieldGroup>

    В данном случае поля будут расположены в две колонки, причем в первой колонке все поля будут шириной 250px, а во второй - 400px.

    Элемент column может иметь следующие атрибуты:

    • width - задает ширину полей данной колонки. По умолчанию ширина полей - 200px. В данном атрибуте ширина может быть задана как в пикселах, так и в процентах от общего размера колонки по горизонтали.

    • flex - число, задающее степень изменения общего размера данной колонки по горизонтали относительно других колонок при изменении ширины всего компонента fieldGroup. Например, можно задать одной колонке flex=1 а другой flex=3.

    • id - необязательный идентификатор колонки, позволяющий ссылаться на нее в случае расширении экрана.

  • field - основной элемент компонента, описывает одно поле компонента.

    Атрибуты элемента field:

    • id - обязательный атрибут, должен содержать либо название атрибута сущности, выводимого в поле, либо произвольный уникальный идентификатор программно определяемого поля. В последнем случае элемент field должен иметь также атрибут custom="true" (см. далее).

    • caption − позволяет задать заголовок поля. Если не задан, будет отображено локализованное название атрибута сущности.

    • visible − позволяет скрыть поле вместе с заголовком.

    • datasource − позволяет задать для данного поля источник данных, отличный от заданного для всего компонента fieldGroup. Таким образом в группе полей могут отображаться атрибуты разных сущностей.

    • optionsDatasource − задает имя источника данных, используемого для формирования списка опций. Данный атрибут можно задать для поля, связанного со ссылочным атрибутом сущности. По умолчанию выбор связанной сущности производится через экран выбора, а если optionsDatasource указан, то связанную сущность можно выбирать из выпадающего списка опций. Фактически указание optionsDatasource приводит к тому, что вместо компонента PickerField в поле используется LookupPickerField.

    • width − позволяет задать ширину поля без учета заголовка. По умолчанию ширина поля - 200px. Ширина может быть задана как в пикселах, так и в процентах от общего размера колонки по горизонтали. Для указания ширины всех полей одновременно можно использовать атрибут width элемента column, описанный выше.

    • custom - установка этого атрибута в true позволяет задать собственное представление поля, или говорит о том, что идентификатор поля не ссылается на атрибут сущности, и компонент, находящийся в поле, будет задан программно с помощью метода addCustomField() компонента FieldGroup (см. ниже).

    • link - установка атрибута в true позволяет отобразить вместо поля выбора сущности ссылку на экран просмотра экземпляра сущности (поддерживается только для Web Client). Такое поведение может быть необходимо, если требуется дать пользователю возможность просматривать связанную сущность, но саму связь он менять не должен.

    • linkScreen - позволяет указать идентификатор экрана, который будет открыт по нажатию на ссылку, включенную свойством link.

    • linkScreenOpenType - задает режим открытия экрана редактирования (THIS_TAB, NEW_TAB или DIALOG).

    • linkInvoke - позволяет заменить открытие окна на вызов метода контроллера.

    Следующие атрибуты элемента field можно применять в зависимости от типа атрибута сущности, отображаемого полем:

    • Если для текстового атрибута сущности задать значение атрибута 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.

Методы интерфейса FieldGroup:

  • Метод addCustomField() используется вместе с атрибутом custom="true" элемента field и позволяет задать собственное представление поля. Он принимает два параметра: идентификатор поля, заданный в атрибуте id элемента field, и реализацию интерфейса FieldGroup.CustomFieldGenerator.

    Метод generateField() интерфейса CustomFieldGenerator вызывается компонентом FieldGroup, и в него передается источник данных и идентификатор поля, для которого зарегистрирован данный генератор. Метод должен вернуть визуальный компонент (или контейнер), который и будет отображен в поле.

    Пример использования:

    @Inject
    protected FieldGroup fieldGroup;
    @Inject
    protected ComponentsFactory componentsFactory;
    
    @Override
    public void init(Map<String, Object> params) {
        fieldGroup.addCustomField("password", new FieldGroup.CustomFieldGenerator() {
            @Override
            public Component generateField(Datasource datasource, String propertyId) {
                PasswordField passwordField = componentsFactory.createComponent(PasswordField.NAME);
                passwordField.setDatasource(datasource, propertyId);
                return passwordField;
            }
        });
    }

  • Метод getFieldComponent() возвращает визуальный компонент, находящийся в поле с указанным идентификатором. Это может потребоваться для дополнительной параметризации компонента, недоступной через атрибуты XML-элемента field, описанные выше.

    Вместо явного вызова getFieldComponent() для получения ссылки на компонент поля в контроллере экрана можно использовать инжекцию. Для этого следует использовать аннотацию @Named с указанием идентификатора самого fieldGroup, и через точку - идентификатора поля.

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

    <fieldGroup id="orderFieldGroup" datasource="orderDs">
        <field id="date"/>
        <field id="customer"/>
        <field id="amount"/>
    </fieldGroup>

    @Named("orderFieldGroup.customer")
    protected PickerField customerField;
    
    @Override
    public void init(Map<String, Object> params) {
        customerField.addOpenAction();
        customerField.removeAction(customerField.getAction(PickerField.ClearAction.NAME));
    }

    Для использования метода getFieldComponent() или инжекции компонентов полей необходимо знать тип компонента, находящегося в поле. В следующей таблице приведено соответствие типов атрибутов сущностей и создаваемых для них компонентов:

    Тип атрибута сущностиДополнительные условияТип компонента поля
    Связанная сущностьЗадан атрибут optionsDatasource LookupPickerField
      PickerField
    Перечисление (enum)  LookupField
    string Задан атрибут mask MaskedField
    Задан атрибут rows TextArea
      TextField
    boolean   CheckBox
    date, dateTime  DateField
    time   TimeField
    int, long, double, decimal  TextField

Все атрибуты fieldGroup:

Все атрибуты field:

Элементы field:

Атрибуты column:

4.5.2.1.7. FileMultiUploadField

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

XML-имя компонента: multiUpload.

Компонент реализован для блоков Web Client и Desktop Client. Для работы веб-версии компонента необходима поддержка браузером технологии Flash.

Рассмотрим пример использования компонента.

  • Объявляем компонент в XML-дескрипторе экрана:

    <multiUpload id="multiUploadField" caption="msg://upload"/>
  • В контроллере экрана инжектируем сам компонент, а также интерфейсы FileUploadingAPI и DataSupplier. Затем в методе init() добавляем компоненту слушатель, который будет реагировать на события успешной загрузки или ошибки:

    @Inject
    protected FileMultiUploadField multiUploadField;
    
    @Inject
    protected FileUploadingAPI fileUploading;
    
    @Inject
    protected DataSupplier dataSupplier;
    
    @Override
    public void init(Map<String, Object> params) {
    multiUploadField.addListener(new FileMultiUploadField.UploadListener() {
        @Override
        public void queueUploadComplete() {
            Map<UUID, String> uploadMap = multiUploadField.getUploadsMap();
            for (Map.Entry<UUID, String> entry : uploadMap.entrySet()) {
                UUID fileId = entry.getKey();
                String fileName = entry.getValue();
                FileDescriptor fd = fileUploading.getFileDescriptor(fileId, fileName);
                // save file to FileStorage
                try {
                    fileUploading.putFileIntoStorage(fileId, fd);
                } catch (FileStorageException e) {
                    new RuntimeException(e);
                }
                // save file descriptor to database
                dataSupplier.commit(fd, null);
            }
            multiUploadField.clearUploads();
        }
    });
    }

    Метод queueUploadComplete() будет вызван компонентом после успешной загрузки всех выбранных файлов во временное хранилище клиентского уровня. В этот момент вызовом метода getUploadsMap() у компонента можно получить мэп идентификаторов файлов во временном хранилище на имена файлов. Далее по этим данным для каждого файла создается соответствующий объект FileDescriptor. Объект com.haulmont.cuba.core.entity.FileDescriptor (не путать с java.io.FileDescriptor) является персистентной сущностью, которая однозначно идентифицирует загруженный файл и впоследствии используется для выгрузки файла из системы.

    Метод FileUploadingAPI.putFileIntoStorage() используется для перемещения загружаемого файла из временного хранилища клиентского уровня в FileStorage. Параметрами этого метода являются идентификатор файла во временном хранилище и объект FileDescriptor.

    После загрузки файла в FileStorage выполняется сохранение экземпляра FileDescriptor в базе данных посредством вызова DataSupplier.commit(). Возвращаемый этим методом сохраненный экземпляр может быть установлен в атрибут какой-либо сущности предметной области, связанной с данным файлом. В данном же случае FileDescriptor просто хранится в системе и дает доступ к файлу через экран Administration > External Files.

    После обработки файлов необходимо очистить список файлов вызовом clearUploads() на случай повторной загрузки.

  • Максимальный размер загружаемого файла определяется свойством приложения cuba.client.maxUploadSizeMb и по умолчанию равен 20Мб. При выборе пользователем файла большего размера выдается соответствующее сообщение и загрузка прерывается.

Атрибуты multiUpload:

4.5.2.1.8. FileUploadField

Компонент FileUploadField позволяет пользователю загружать файлы на сервер. Компонент представляет собой кнопку, при нажатии на которую на экране отображается стандартное для операционной системы окно, в котором можно выбрать один файл. Чтобы дать пользователю возможность загружать сразу несколько файлов, используйте компонент FileMultiUploadField.

XML-имя компонента: upload.

Компонент реализован для блоков Web Client и Desktop Client.

Рассмотрим пример использования компонента.

  • Объявляем компонент в XML-дескрипторе экрана:

    <upload id="uploadField" caption="msg://upload"/>
  • В контроллере экрана инжектируем сам компонент, а также интерфейсы FileUploadingAPI и DataSupplier. Затем в методе init() добавляем компоненту слушатель, который будет реагировать на события успешной загрузки или ошибки:

    @Inject
    protected FileUploadField uploadField;
    
    @Inject
    protected FileUploadingAPI fileUploading;
    
    @Inject
    protected DataSupplier dataSupplier;
    
    @Override
    public void init(Map<String, Object> params) {
    uploadField.addListener(new FileUploadField.ListenerAdapter() {
        @Override
        public void uploadSucceeded(Event event) {
            FileDescriptor fd = uploadField.getFileDescriptor();
            try {
                // save file to FileStorage
                fileUploading.putFileIntoStorage(uploadField.getFileId(), fd);
            } catch (FileStorageException e) {
                throw new RuntimeException(e);
            }
            // save file descriptor to database
            dataSupplier.commit(fd, null);
    
            showNotification("File uploaded: " + uploadField.getFileName(), NotificationType.HUMANIZED);
        }
    
        @Override
        public void uploadFailed(Event event) {
            showNotification("File upload error", NotificationType.HUMANIZED);
        }
    });
    }

    Метод uploadSucceeded() будет вызван компонентом после успешной загрузки файла во временное хранилище клиентского уровня. В этот момент у компонента можно получить объект FileDescriptor, соответствующий загруженному файлу. Объект com.haulmont.cuba.core.entity.FileDescriptor (не путать с java.io.FileDescriptor) является персистентной сущностью, которая однозначно идентифицирует загруженный файл и впоследствии используется для выгрузки файла из системы.

    Метод FileUploadingAPI.putFileIntoStorage() используется для перемещения загружаемого файла из временного хранилища клиентского уровня в FileStorage. Параметрами этого метода являются идентификатор файла во временном хранилище и объект FileDescriptor. Оба эти параметра предоставляет FileUploadField.

    После загрузки файла в FileStorage выполняется сохранение экземпляра FileDescriptor в базе данных посредством вызова DataSupplier.commit(). Возвращаемый этим методом сохраненный экземпляр может быть установлен в атрибут какой-либо сущности предметной области, связанной с данным файлом. В данном же случае FileDescriptor просто хранится в системе и дает доступ к файлу через экран Administration > External Files.

    Метод uploadFailed() вызывается компонентом FileUploadField в случае ошибки загрузки файла во временное хранилище клиентского уровня.

  • Максимальный размер загружаемого файла определяется свойством приложения cuba.client.maxUploadSizeMb и по умолчанию равен 20Мб. При выборе пользователем файла большего размера выдается соответствующее сообщение и загрузка прерывается.

Атрибуты upload:

4.5.2.1.9. Filter

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

Filter должен быть связан с источником данных collectionDatasource содержащим запрос на JPQL. Принцип действия фильтра основан на модификации этого запроса в соответствии с критериями, заданными пользователем. Таким образом фильтрация осуществляется на уровне БД при выполнении транслированного из JPQL в SQL запроса, и на Middleware и клиентский уровень загружаются только отобранные данные.

4.5.2.1.9.1. Использование фильтра

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

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

Для того чтобы создать быстрый фильтр, нажмите на ссылку Add search condition (Добавить условие поиска). Отобразится экран выбора условий:

Рассмотрим возможные типы условий:

  • Properties (Атрибуты) – атрибуты данной сущности и связанных с ней сущностей. Отображаются персистентные атрибуты, явно заданные в элементе property XML-описателя фильтра, либо соответствующие правилам, указанным в элементе properties (см. ниже).

  • Custom conditions (Специальные условия) – условия, заданные разработчиком в элементах custom XML-описателя фильтра.

  • Create new (Создать новое) – позволяет создать новое произвольное условие на JPQL. Данный пункт доступен пользователю, если у него есть специфическое разрешение cuba.gui.filter.customConditions.

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

Быстрый фильтр можно сохранить для повторного использования в дальнейшем. Для этого нажмите на кнопку настроек фильтра и выберите Save/Save as (Сохранить/Сохранить как). Во всплывающем окне задайте имя нового фильтра:

Фильтр будет сохранен в выпадающем меню кнопки Search (Поиск).

Пункт меню Reset filter (Сбросить фильтр) позволяет сбросить все текущие условия поиска.

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

  • Save (Сохранить) – сохранить изменения в текущем фильтре.

  • Save as (Сохранить как) – сохранить фильтр под новым именем.

  • Edit (Редактировать) – открыть редактор фильтра (см. ниже).

  • Make default (Установить по умолчанию) – установить фильтр по умолчанию для данного экрана. Фильтр будет автоматически выводиться на панель при каждом открытии экрана.

  • Remove (Удалить) – удалить текущий фильтр.

  • Pin applied (Закрепить) – использовать результаты последнего поиска для последовательной фильтрации данных (см. Раздел 4.5.2.1.9.5, «Последовательное наложение фильтров»).

  • Save as search folder (Сохранить как папку поиска) – создать папку поиска на основе текущего фильтра.

  • Save as application folder (Сохранить как папку приложения) – создать папку приложения на основе текущего фильтра. Эта опция доступна только пользователям со специфическим разрешением cuba.gui.appFolder.global.

Опция Edit открывает редактор фильтра, который дает возможность расширенной настройки текущего фильтра:

Название фильтра указывается в поле Filter name (Имя фильтра). Это имя будет отображаться в списке доступных фильтров для текущего экрана.

Фильтр можно сделать global (то есть доступным для всех пользователей) с помощью установки флажка Available for all users (Общий) для всех пользователей, или установить текущий фильтр в качестве фильтра по умолчанию с помощью установки флажка Default (По умолчанию).

В дереве содержатся условия фильтра. Условия можно добавлять с помощью кнопки Add (Добавить) менять местами при помощи кнопок / или удалять с помощью кнопки Remove (Удалить).

Группировку условий по И или ИЛИ можно добавить с помощью соответствующих кнопок. Все добавленные на верхний уровень (то есть без явной группировки) условия объединяются по И.

При выборе условия в дереве в правой части редактора открывается список его свойств.

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

Свойство Width позволяет задать ширину поля ввода параметра для текущего условия. По умолчанию, условия на панели фильтров отображаются в три колонки. Ширина поля равняется количеству колонок, которое оно может занять (1, 2 или 3).

Значение параметра текущего условия по умолчанию можно задать в поле Default value (Значение по умолчанию).

Специальный заголовок условия фильтрации можно задать в поле Caption (Заголовок).

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

При нажатии ОК в редакторе фильтра изменения сохраняются только для текущего быстрого поиска. Для того чтобы сохранить их для повторного использования в дальнейшем, нажмите на кнопку настройки фильтра и выберите Сохранить/Сохранить как. Иначе при закрытии экрана просмотра экземпляров сущности все изменения будут утеряны.

4.5.2.1.9.2. Описание компонента Filter

XML-имя компонента: filter.

Компонент реализован для блоков Web Client и Desktop Client.

Пример объявления компонента в XML-дескрипторе экрана:

<dsContext>
    <collectionDatasource id="carsDs" class="com.company.sample.entity.Car" view="carBrowse">
        <query>
            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>

Здесь в элементе dsContext определен источник данных collectionDatasource, который выбирает экземпляры сущности Car с помощью JPQL запроса. Для компонента filter в его атрибуте datasource указан фильтруемый источник данных. Данные отображаются компонентом Table, связанным с этим же источником.

Элемент filter может содержать вложенные элементы. Все они описывают условия, доступные пользователю для выбора в диалоге добавления условий:

  • properties - позволяет сделать доступными сразу несколько атрибутов сущности. Данный элемент может иметь следующие атрибуты:

    • include - обязательный атрибут, содержит регулярное выражение, которому должно соответствовать имя атрибута сущности.

    • exclude - содержит регулярное выражение, при соответствии которому атрибут сущности исключается из ранее включенных с помощью include.

    Например:

    <filter id="transactionsFilter" datasource="transactionsDs">
        <properties include=".*" exclude="(masterTransaction)|(authCode)"/>
    </filter>

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

  • property - явно включает атрибут сущности по имени. Данный элемент может иметь следующие атрибуты:

    • name - обязательный атрибут, содержит имя включаемого атрибута сущности. Может быть путем (через ".") по графу сущностей. Например:

      <filter id="transactionsFilter" datasource="transactionDs" applyTo="table">
          <properties include=".*" exclude="(masterTransaction)|(authCode)"/>
          <property name="creditCard.maskedPan" caption="msg://EmbeddedCreditCard.maskedPan"/>
          <property name="creditCard.startDate" caption="msg://EmbeddedCreditCard.startDate"/>
      </filter>

    • caption - локализованное название атрибута сущности для отображения условия фильтра. Как правило, представляет из себя строку с префиксом msg:// по правилам MessageTools.loadString().

      Если в атрибуте name указан путь (через ".") по графу сущностей, то атрибут caption является обязательным.

    • paramWhere − задает выражение на JPQL для отбора списка значений параметра условия, если параметр является связанной сущностью. Вместо алиаса сущности параметра в выражении нужно использовать метку (placeholder) {E}.

      Например, предположим, что сущность Car имеет ссылку на сущность Model. Тогда список возможных значений параметра может быть ограничен только моделями Audi:

      <filter id="carsFilter" datasource="carsDs">
          <property name="model" paramWhere="{E}.manufacturer = 'Audi'"/>
      </filter>

      В выражении JPQL можно использовать параметры экрана, атрибуты сессии, а также компоненты экрана, в том числе отображающие другие параметры. Правила задания параметров запроса описаны в Раздел 4.5.3.2, «Запросы в CollectionDatasourceImpl».

      Пример использования параметра сессии и параметра экрана:

      {E}.createdBy = :session$userLogin and {E}.name like :param$groupName

      Используя paramWhere можно вводить зависимости между параметрами. Например, предположим, что Manufacturer является отдельной сущностью. То есть Car ссылается на Model, которая в свою очередь ссылается на Manufacturer. Тогда для фильтра по Car можно создать два условия: первое для выбора Manufacturer и второе для выбора Model. Чтобы ограничить список моделей выбранным перед этим производителем, добавьте в выражение paramWhere параметр:

      {E}.manufacturer.id = :component$filter.model_manufacturer90062

      Здесь параметр ссылается на компонент, отображающий параметр Manufacturer. Имя компонента, отображающего параметр условия, можно узнать, вызвав контекстное меню на строке таблицы условий в редакторе фильтра:

    • paramView − задает представление, с которым будет загружаться список значений параметра условия, если параметр является связанной сущностью. Например, _local. Если не указано, используется _minimal.

  • custom - элемент, определяющий произвольное условие. Содержимым элемента должно быть выражение на JPQL (возможно использование JPQL Macros), которое будет добавлено в условие where запроса источника данных. Вместо алиаса отбираемой сущности в выражении нужно использовать метку (placeholder) {E}. Параметр условия может быть только один, и если он есть, обозначается символом ?.

    Пример фильтра с произвольными условиями:

    <filter id="carsFilter" datasource="carsDs">
        <properties include=".*"/>
        <custom name="vin" paramClass="java.lang.String" caption="msg://vin">
          {E}.vin like ?
        </custom>
        <custom name="colour" 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>

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

    Атрибуты элемента custom:

    • name − обязательный атрибут - имя условия.

    • caption − обязательный атрибут - локализованное название условия. Как правило, представляет из себя строку с префиксом msg:// по правилам 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

      будет трансформирован в следующий:

      select c from sample$Car c join c.repairs cr
      where (cr.description like ?)
      order by c.createTs
    • paramWhere − задает выражение на JPQL для отбора списка значений параметра условия, если параметр является связанной сущностью. См. описание одноименного атрибута элемента property.

    • paramView − задает представление, с которым будет загружаться список значений параметра условия, если параметр является связанной сущностью. Например, _local. Если не указано, используется _minimal.

Атрибуты filter:

  • editable - если значение этого атрибута равно false, то кнопка Фильтр скрывается.

  • required - если значение этого атрибута равно true, то в списке фильтров значение <без фильтрации> не отображается, и пользователь обязательно должен выбрать один из доступных фильтров. Если для экрана не установлен фильтр по умолчанию, то в списке выбора фильтра автоматически устанавливается первый созданный фильтр.

  • manualApplyRequired − определяет, в какой момент будет применяться фильтр. Если значение атрибута равно false, то сразу при открытии экрана будет применяться фильтр по умолчанию. Если фильтр по умолчанию отсутствует, то установка значения false для атрибута теряет смысл. Если значение атрибута равно true, то фильтр будет применяться только после нажатия на кнопку Применить.

    Данный атрибут имеет приоритет над свойством приложения cuba.gui.genericFilterManualApplyRequired.

  • useMaxResults − ограничивает размер страницы загружаемых в источник данных экземпляров сущности. По умолчанию true.

    Если значение этого атрибута равно false, то фильтр не будет отображать поле Показывать строк. Количество записей в источнике данных (и соответственно, показываемых таблицей) будет ограничено только параметром MaxFetchUI механизма статистики сущностей, по умолчанию - 10000.

    Если данный атрибут не указан, или равен true, то поле Показывать строк отображается, если у пользователя также есть специфическое разрешение cuba.gui.filter.maxResults. Если разрешение cuba.gui.filter.maxResults отсутствует, то фильтр будет принудительно отбирать только первые N строк без возможности пользователя отключить это или указать другое N. Число N определяется параметрами FetchUI, DefaultFetchUI, получаемыми из механизма статистики сущностей.

    На рисунке далее показан вид фильтра со значением атрибута useMaxResults="true", запретом специфического разрешения cuba.gui.filter.maxResults и параметром DefaultFetchUI=2

  • textMaxResults - позволяет использовать текстовое поле вместо выпадающего списка в качестве поля Показывать строк. По умолчанию false.

  • folderActionsEnabled − при указании значения false позволяет скрыть следующие действия с фильтром: Сохранить как папку поиска, Сохранить как папку приложения. По умолчанию значение атрибута равно true, действия Сохранить как папку поиска, Сохранить как папку приложения доступны.

  • applyTo − необязательный атрибут, содержит идентификатор компонента, с которым связан фильтр. Используется в случае, когда необходимо иметь доступ к представлениям связанного компонента-таблицы. Например, сохраняя фильтр как папку поиска или как папку приложения, можно указать, какое представление будет применятся при просмотре этой папки.

  • caption - позволяет задать заголовок панели фильтров.

  • columnsQty - задает количество колонок с условиями для конкретного фильтра. Значение по умолчанию - 3.

Все атрибуты filter:

Элементы filter:

Атрибуты элемента properties:

Атрибуты элемента property:

Атрибуты элемента custom:

4.5.2.1.9.3. Права пользователей
  • Для создания/изменения/удаления глобальных (доступных всем пользователям) фильтров пользователь должен иметь разрешение cuba.gui.filter.global.

  • Для создания/изменения custom условий пользователь должен иметь разрешение cuba.gui.filter.customConditions.

  • Чтобы иметь возможность изменять максимальное количество строк на странице таблицы с помощью флажка и поля Show first N rows пользователь должен иметь разрешение cuba.gui.filter.maxResults. См. также атрибут фильтра useMaxResults.

Информация о том, как настраивать специфические разрешения, приведена в руководстве Подсистема безопасности.

4.5.2.1.9.4. Внешние параметры для управления фильтрами

Общесистемные параметры

Следующие свойства приложения влияют на поведение фильтров:

Параметры вызова экрана

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

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

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

Пример описателя пункта главного меню, устанавливающего в открываемом экране sample$Car.browse в компоненте carsFilter фильтр с кодом FilterByVIN, с подстановкой в параметр условия component$carsFilter.vin79216 значения TMA:

<item id="sample$Car.browse">
<param name="carsFilter" value="FilterByVIN"/>
<param name="component$carsFilter.vin79216" value="TMA"/>
</item>

Следует отметить, что фильтр с установленным полем CODE обладает особыми свойствами:

  • Его не могут редактировать пользователи.

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

4.5.2.1.9.5. Последовательное наложение фильтров

Последовательное наложение фильтров

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

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

  • Декомпозировать сложные фильтры.

  • Применять фильтры на записи, отобранные с помощью папок приложения или поиска.

Чтобы применить этот механизм в пользовательском интерфейсе, выберите и примените один из фильтров. Затем нажмите на кнопку настроек фильтра и выберите Pin applied (Закрепить). Фильтр закрепится в верхней части панели фильтров. Далее можно применить к выбранным записям другой фильтр. Так последовательно можно накладывать друг на друга любое количество фильтров. Также фильтры можно удалять последовательно с помощью кнопки

Механизм последовательного наложения фильтров основан на возможности DataManager выполнять последовательные запросы.

4.5.2.1.10. GroupTable

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

XML-имя компонента: groupTable.

Компонент реализован только для блока Web Client. В Desktop 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>
    <rows datasource="ordersDs"/>
</groupTable>

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

При включенном атрибуте aggregatable таблица отображает результаты агрегации по всем строкам в дополнительной строке вверху, а также результаты агрегации по группам. Отображение агрегации по всем строкам можно отключить, установив false в атрибуте showTotalAggregation.

В остальном функциональность GroupTable аналогична простой таблице Table.

Атрибуты groupTable:

Элементы groupTable:

Элементы columns:

Атрибуты column:

Элементы column:

Атрибуты rows:

4.5.2.1.11. Label

Надпись (Label) − текстовый компонент, отображающий статический текст либо значение атрибута сущности.

XML-имя компонента: label

Компонент Label реализован для блоков Web Client и Desktop Client.

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

<label value="msg://orders"/>

Атрибут value предназначен для задания текста надписи.

В веб клиенте текст, содержащийся в атрибуте value, будет разбит на несколько строк, если по длине он превысит значение атрибута width. Поэтому для отображения многострочной надписи, достаточно указать абсолютное значение атрибута width. Если текст надписи слишком длинный, а значение атрибута width не определено, то текст будет урезан.

<label
   value="Надпись, которая должна быть разбита на несколько строк"
   width="200px"/>

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

<label id="dynamicLabel"/>
@Inject
private Label dynamicLabel;

public void init(Map<String, Object> params) {
dynamicLabel.setValue("Some value");
}

Компонент Label может отображать значение атрибута сущности. Для этого используются атрибуты datasource и property. Например:

<dsContext>
<datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
</dsContext>
<layout>
...
<label datasource="customerDs" property="name"/>

В данном случае компонент отображает атрибут name сущности Customer, находящейся в источнике данных customerDs.

Атрибут htmlEnabled указывает, каким образом будет рассматриваться значение атрибута value: как html-код, при htmlEnabled="true", или как строка. Обратите внимание, что не все html-теги поддерживаются в десктоп-реализации экрана.

Атрибуты label:

Элементы label:

4.5.2.1.12. Link

Ссылка (Link) − компонент-гиперссылка, позволяющая открывать внешние веб-ресурсы единообразно для веб и десктоп клиента.

XML-имя компонента: link

Пример XML-описания компонента link:

<link caption="Link" url="https://www.cuba-platform.com" target="_blank"/>

Атрибуты link:

  • url - адрес ресурса.

  • target - для веб клиента задает способ открытия страницы, аналогичен атрибуту target HTML-тега <a>.

Другие атрибуты Link: align | caption | description | enable | id | icon | stylename | visible | width

4.5.2.1.13. LinkButton

Кнопка-ссылка (LinkButton) − кнопка, выглядящая как гиперссылка.

XML-имя компонента: linkButton

Компонент кнопки-ссылки реализован для блоков Web Client и Desktop Client.

Кнопка-ссылка может содержать текст или пиктограмму (или и то и другое). На рисунке ниже отражены разные виды кнопок.

Кнопка-ссылка отличается от обычной кнопки Button только своим внешним видом. Все свойства и поведение идентичны описанным для Button.

Пример XML-описания кнопки-ссылки, вызывающей метод someMethod() контроллера, с надписью (атрибут caption), всплывающей подсказкой (атрибут description) и пиктограммой (атрибут icon):

<linkButton id="linkButton"
        caption="msg://linkButton"
        description="Press me"
        icon="icons/save.png"
        invoke="someMethod"/>

Атрибуты linkButton:

4.5.2.1.14. LookupField

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

XML-имя компонента: lookupField.

Компонент LookupField реализован для блоков Web Client и Desktop Client.

  • Простейший вариант использования LookupField - выбор значения перечисления (enum) для атрибута сущности. Например, сущность Role имеет атрибут type типа RoleType, который является перечислением. Тогда для редактирования этого атрибута можно использовать LookupField следующим образом:

    <dsContext>
    <datasource id="roleDs" class="com.haulmont.cuba.security.entity.Role" view="_local"/>
    </dsContext>
    <layout>
    <lookupField datasource="roleDs" property="type"/>

    Как видно из примера, в экране описывается источник данных roleDs для сущности Role. В компоненте lookupField в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено. В данном случае атрибут является перечислением, и в выпадающем списке будут отображены локализованные названия всех значений этого перечисления.

  • Аналогично можно использовать LookupField для выбора экземпляра связанной сущности. Для формирования списка опций используется атрибут optionsDatasource:

    <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>
    <lookupField datasource="carDs" property="colour" optionsDatasource="coloursDs"/>

    В данном случае компонент отобразит отобразит имена экземпляров сущности Colour, находящихся в источнике данных colorsDs, а выбранное значение подставится в атрибут colour сущности Car, находящейся в источнике данных carDs.

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

  • Список опций компонента может быть задан произвольно с помощью методов setOptionsList() и setOptionsMap(), либо с помощью XML-атрибута optionsDatasource.

    • Метод setOptionsList() позволяет программно задать список опций компонента. Для этого объявляем компонент в XML-дескрипторе:

      <lookupField id="numberOfSeatsField" datasource="modelDs" property="numberOfSeats"/>

      Затем инжектируем компонент в контроллер и в методе 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);
      }

      В выпадающем списке компонента отобразятся числа 2, 4, 5, 7. Выбранное число подставится в атрибут numberOfSeats сущности, находящейся в источнике данных modelDs.

    • Метод setOptionsMap() позволяет задать строковые названия и значения опций по отдельности. Например, для описанного в XML-дескрипторе компонента numberOfSeatsField в методе init() контроллера задаем мэп опций:

      @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);
      numberOfSeatsField.setOptionsMap(map);
      }

      В выпадающем списке компонента отобразятся строки two, four, five, seven. Однако значением компонента будет число, соответствующее выбранной строке. Оно и подставится в атрибут numberOfSeats сущности, находящейся в источнике данных modelDs.

  • С помощью атрибута filterMode можно задать тип фильтрации опций при вводе пользователя:

    • NO − нет фильтрации.

    • STARTS_WITH − по началу фразы.

    • CONTAINS − по любому вхождению (используется по умолчанию).

  • Если у компонента LookupField не установлен атрибут required, и если связанный атрибут сущности не объявлен как обязательный, то в списке опций компонента присутствует пустая строка, при выборе которой компонент возвращает значение null. Атрибут nullName позволяет задать строку, отображаемую в этом случае вместо пустой. Пример использования:

    <lookupField datasource="carDs" property="colour" optionsDatasource="coloursDs" nullName="(none)"/>

    В данном случае вместо пустой строки отобразится строка (none), при выборе которой в связанный атрибут сущности подставится значение null.

    При программном задании списка опций методом setOptionsList() можно одну из опций передать в метод setNullOption(). Тогда при ее выборе пользователем значением компонента будет null.

  • Компонент LookupField способен обрабатывать ввод пользователя при отсутствии подходящей опции в списке. Для этого используются методы setNewOptionAllowed() и setNewOptionHandler(). Например:

    @Inject
    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);
        }
    });
    }

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

    Вместо имплементации интерфейса LookupField.NewOptionHandler для обработки ввода пользователя можно использовать XML-атрибут newOptionHandler с указанным в нем методом контроллера. Данный метод должен иметь два параметра - первый типа LookupField, второй типа String. В них будут переданы соответственно экзампляр компонента и введенное пользователем значение.

Атрибуты lookupField:

align | caption | captionProperty | datasource | description | editable | enable | filterMode | height | id | inputPrompt | newOptionHandler | nullName | optionsDatasource | property | required | requiredMessage | stylename | visible | width

Элементы lookupField:

validator

4.5.2.1.15. LookupPickerField

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

XML-имя компонента: lookupPickerField.

Компонент реализован для блоков Web Client и Desktop Client.

LookupPickerField является по сути гибридом LookupField и PickerField, поэтому все описанное для этих интерфейсов верно и для него. Исключением является список действий по умолчанию, добавляемых при определении компонента в XML: для LookupPickerField это действия lookup и open .

Пример использования LookupPickerField для выбора значения ссылочного атрибута colour сущности Car:

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

Атрибуты lookupPickerField:

align | caption | captionProperty | datasource | description | editable | enable | filterMode | height | id | inputPrompt | metaClass | nullName | optionsDatasource | property | required | requiredMessage | stylename | visible | width

Элементы lookupPickerField:

actions | validator

4.5.2.1.16. MaskedField

Текстовое поле, в которое данные вводятся в определенном формате. MaskedField удобно использовать, например, для ввода телефонных номеров.

XML-имя компонента: maskedField.

Компонент MaskedField реализован только для блока Web Client.

MaskedField в основном повторяет функциональность TextField, за исключением того, что ему нельзя установить datatype. То есть MaskedField предназначен для работы только с текстом и строковыми атрибутами сущностей. MaskedField имеет следующие специфические атрибуты:

  • mask - задает маску для поля. Чтобы задать маску, используются следующие символы:

    • # - цифра

    • U - буква верхнего регистра

    • L - буква нижнего регистра

    • ? - буква

    • А - буква или цифра

    • * - любой символ

    • H - hex символ в верхнем регистре

    • h - hex символ в нижнем регистре

    • ~ - знак + или -

  • valueMode - определяет формат возвращаемого значения (с маской, или без) и может принимать значение masked или clear.

Пример текстового поля с маской для ввода номеров телефонов:

<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);
}

Атрибуты maskedField:

Элементы maskedField:

4.5.2.1.17. OptionsGroup

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

XML-имя компонента: optionsGroup.

Компонент OptionsGroup реализован для блоков Web Client и Desktop Client.

  • Простейший вариант использования OptionsGroup - выбор значения перечисления (enum) для атрибута сущности. Например, сущность Role имеет атрибут type типа RoleType, который является перечислением. Тогда для редактирования этого атрибута можно использовать OptionsGroup следующим образом:

    <dsContext>
    <datasource id="roleDs" class="com.haulmont.cuba.security.entity.Role" view="_local"/>
    </dsContext>
    <layout>
    <optionsGroup datasource="roleDs" property="type"/>

    Как видно из примера, в экране описывается источник данных roleDs для сущности Role. В компоненте optionsGroup в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено.

    В результате компонент примет следующий вид:

  • Список опций компонента может быть задан произвольно с помощью методов setOptionsList() и setOptionsMap(), либо с помощью XML-атрибута optionsDatasource.

    • Метод setOptionsList() позволяет программно задать список опций компонента. Для этого объявляем компонент в XML-дескрипторе:

      <optionsGroup id="numberOfSeatsField"/>

      Затем инжектируем компонент в контроллер и в методе init() задаем ему список опций:

      @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() контроллера задаем мэп опций:

      @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, а не строки, отображаемые на экране.

    • Компонент может брать список опций из источника данных. Для этого используется атрибут optionsDatasource. Например:

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

      В данном случае компонент coloursField отобразит имена экземпляров сущности Colour, находящихся в источнике данных coloursDs, а его метод getValue() вернет выбранный экземпляр сущности.

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

  • С помощью атрибута multiselect можно переключить OptionsGroup в режим множественного выбора. Если multiselect включен, то компонент отображается как группа независимых флажков, а значением компонента является список выбранных опций.

    Например, создадим в XML-дескрипторе экрана компонент:

    <optionsGroup id="roleTypesField" multiselect="true"/>

    И в контроллере зададим для него список опций - значения перечисления RoleType:

    @Inject
    protected OptionsGroup roleTypesField;
    
    @Override
    public void init(Map<String, Object> params) {
    roleTypesField.setOptionsList(Arrays.asList(RoleType.values()));
    }

    Компонент примет следующий вид:

    В данном случае метод getValue() компонента вернет объект типа java.util.List, содержащий значения RoleType.READONLY и RoleType.DENYING.

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

  • Атрибут orientation задает расположение элементов группы. По умолчанию элементы располагаются по вертикали. Значение horizontal задает горизонтальное расположение.

Атрибуты optionsGroup:

Элементы optionsGroup:

4.5.2.1.18. PasswordField

Текстовое поле, которое вместо символов, введенных пользователем, отображает эхо-символы.

XML-имя компонента: passwordField.

PasswordField реализован для блоков Web Client и Desktop Client.

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

Пример использования:

<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);
}

Атрибуты passwordField:

Элементы passwordField:

4.5.2.1.19. PickerField

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

XML-имя компонента: pickerField.

Компонент PickerField реализован для блоков Web Client и Desktop Client.

  • Как правило, PickerField используется для работы со ссылочными атрибутами сущностей. При этом компоненту достаточно указать атрибуты datasource и property:

    <dsContext>
    <datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
    </dsContext>
    <layout>
    <pickerField datasource="carDs" property="colour"/>

    Как видно из примера, в экране описывается источник данных carDs для некоторой сущности Car, имеющей атрибут colour. В элементе pickerField в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в компоненте. Атрибут сущности должен являться ссылкой на другую сущность, в приведенном примере это Colour.

  • Для PickerField можно определить произвольное количество действий, отображаемых кнопками справа. Это можно сделать как в XML-дескрипторе с помощью вложенного элемента actions, так и программно в контроллере методом addAction().

    • Существуют стандартные действия, определенные перечислением PickerField.ActionType: lookup, clear, open. Они выполняют соответственно выбор связанной сущности, очистку поля и открытие экрана редактирования выбранной связанной сущности. Для стандартных действий в XML не нужно определять никаких атрибутов, кроме идентификатора. Если при объявлении компонента никаких действий в элементе actions не задано, загрузчик XML определит для него действия lookup и clear. Чтобы добавить к действиям по умолчанию, например, действие open, нужно определить элемент actions следующим образом:

      <pickerField datasource="carDs" property="colour"/>
      <actions>
          <action id="lookup"/>
          <action id="open"/>
          <action id="clear"/>
      </actions>
      </pickerField>

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

      Для программного задания стандартных действий служат методы addLookupAction(), addOpenAction() и addClearAction(). Если компонент определен в XML-дескрипторе без вложенного элемента actions, то достаточно добавить недостающие действия:

      @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();
      }

      Стандартные действия можно параметризовать. В XML-дескрипторе возможности для этого ограничены: существует только атрибут openType, в котором можно задать режим открытия экрана выбора (для LookupAction) или редактирования (для OpenAction).

      При программном создании действий можно задать любые свойства объектов PickerField.LookupAction, PickerField.OpenAction и PickerField.ClearAction, возвращаемых методами добавления стандартных действий. Например, так можно задать специфический экран выбора:

      PickerField.LookupAction lookupAction = customerField.addLookupAction();
      lookupAction.setLookupScreen("customerLookupScreen");

      Подробнее см. JavaDocs классов стандартных действий.

    • Произвольные действия в XML-дескрипторе также определяются во вложенном элементе actions, например:

      <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
          public void actionPerform(Component component) {
              showColour(colourField.getValue());
          }
          @Override
          public String getCaption() {
              return "";
          }
          @Override
          public String getIcon() {
              return "icons/show.png";
          }
      });
      }

      Декларативное и программное создание действий подробно описано в Раздел 4.5.4, «Действия. Интерфейс Action».

  • Компонент PickerField можно использовать без непосредственной привязки к данным, то есть без указания datasource и property. В этом случае для указания типа сущности, с которой должен работать PickerField, используется атрибут metaClass. В нем необходимо указать имя сущности в метаданных, например:

    <pickerField id="colourField" metaClass="sample$Colour"/>

    Экземпляр выбранной сущности можно получить, инжектировав компонент в контроллер и вызвав его метод getValue().

    Для правильной работы компонента PickerField необходима либо установка атрибута metaClass, либо одновременная установка атрибутов datasource и property.

  • В компоненте PickerField можно использовать горячие клавиши: см. Раздел 4.5.11, «Горячие клавиши».

Атрибуты pickerField:

Элементы pickerField:

4.5.2.1.20. PopupButton

Кнопка с выпадающим списком действий.

XML-имя компонента: popupButton.

Компонент реализован для блоков Web Client и Desktop Client.

Кнопка с выпадающим списком действий может содержать текст или пиктограмму (или и то и другое). На рисунке ниже отражены разные виды кнопок.

Пример кнопки с выпадающим списком, содержащим два действия:

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

Кнопка имеет надпись, заданную с помощью атрибута caption, и всплывающую подсказку, определенную в атрибуте description. Выпадающий список действий задан в элемене actions. PopupButton отображает только следующие свойства действий: caption, enable, visible. Свойства description, icon, shortcut игнорируются.

Атрибуты popupButton:

Элементы popupButton:

4.5.2.1.21. ProgressBar

Компонент ProgressBar служит для отображения хода выполнения некоторого длительного процесса.

XML-имя компонента: progressBar

Компонент реализован для блоков Web Client и Desktop Client.

Пример использования компонента совместно с механизмом фоновых задач:

<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 {
        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();
}

Здесь в методе BackgroundTask.progress(), выполняемом в UI-потоке, компоненту ProgressBar устанавливается текущее значение. Значением компонента должно быть число типа float от 0.0 до 1.0.

Если выполняемый процесс не может передавать информацию о прогрессе, то с помощью атрибута indeterminate можно задать отображение неопределенного состояния индикатора. Если значение атрибута равно true, то индикатор отображает неопределенное состояние. По умолчанию false. Например:

<progressBar id="progressBar" width="100%" indeterminate="true"/>

Атрибуты progressBar:

4.5.2.1.22. Related Entities

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

XML-имя компонента: relatedEntities

Компонент реализован для блоков Web Client и Desktop Client.

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

По умолчанию для выбранного в списке класса сущности открывается браузер сущности, определенный по соглашениям (.browse, .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"/>
              <button id="removeBtn"
                      action="invoiceTable.remove"/>
              <relatedEntities for="invoiceTable"
                               openType=”NEW_TAB”>
              <property name="invoiceItems"
                        screen="sales$InvoiceItem.lookup"
                        filterCaption="msg://invoiceItems"/>
          </relatedEntities>
      </buttonsPanel>
      

Атрибут for является обязательным. В нем указывается идентификатор таблицы.

Атрибут openType=”NEW_TAB” устанавливает режим открытия браузера (новая вкладка). По умолчанию браузер открывается в текущей вкладке.

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

Атрибуты property:

  • name - имя атрибута текущей сущности, ссылающегося на связанную сущность

  • screen - идентификатор браузера, открывающегося при выборе сущности в списке

  • filterCaption - имя динамически формируемого фильтра

Атрибут exclude позволяет исключить определенные связанные сущности из числа отображаемых. В качестве значения указываются ссылочные атрибуты текущей сущности, разделенные запятыми.

Рисунок 22. Компонент Related Entities в таблице

Компонент Related Entities в таблице

Рисунок 23. Браузер связанных сущностей в новой вкладке

Браузер связанных сущностей в новой вкладке

Все атрибуты relatedEntities:

Атрибуты property:

4.5.2.1.23. RichTextArea

Текстовая область для отображения и ввода форматированного текста.

XML-имя компонента: richTextArea

Компонент RichTextArea реализован только для блока Web Client.

RichTextArea в основном повторяет функциональность TextField , за исключением того, что ему нельзя установить datatype. То есть RichTextArea предназначен для работы только с текстом и строковыми атрибутами сущностей.

К тексту, вводимому в компоненте RichTextArea, можно применять средства для форматирования: изменять начертание шрифта, его размер, гарнитуру − с помощью элементов управления, расположенных в верхней части компонента.

Атрибуты richTextArea:

4.5.2.1.24. SearchPickerField

Компонент SearchPickerField служит для поиска экземпляров сущностей по вводимой пользователем строке. Пользователю достаточно ввести несколько символов и нажать клавишу Enter. Если поиск дал несколько совпадений, найденные значения отображаются в виде выпадающего списка. Если же критерию поиска соответствует только один экземпляр, он сразу становится значением компонента. SearchPickerField позволяет также выполнять действия нажатием на кнопки справа.

XML-имя компонента: searchPickerField.

Компонент реализован для блоков Web Client и Desktop Client.

  • Для работы компонента SearchPickerField необходимо создать collectionDatasource, и задать в нем запрос, содержащий условия поиска. Условие обязательно должно содержать параметр с именем custom$searchString - именно в него компонент передает введенную пользователем подстроку при нажатии Enter. Источник данных с условием поиска должен быть указан в атрибуте optionsDatasource компонента. Например:

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

    В данном случае компонент будет искать экземпляры сущности Colour по вхождению подстроки в ее атрибут name. Префикс (?i) служит для регистро-независимого поиска (см. Раздел 4.5.3.2.4, «Поиск подстроки без учета регистра»). Выбранное значение подставится в атрибут colour сущности Car, находящейся в источнике данных carDs.

  • С помощью атрибута minSearchStringLength можно задать минимальное количество символов, которое должен ввести пользователь для поиска значения.

  • В контроллере экрана для компонента можно реализовать методы, вызываемые в двух случаях:

    • если количество введенных символов меньше значения атрибута minSearchStringLength.

    • если поиск введенных пользователем символов не дал результатов.

    Пример реализации методов для вывода на экран сообщений:

    @Inject
    private SearchPickerField colourField;
    
    @Override
    public void init(Map<String, Object> params) {
    colourField.setSearchNotifications(new SearchField.SearchNotifications() {
        @Override
        public void notFoundSuggestions(String 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);
        }
    });
    }
  • SearchPickerField реализует интерфейсы LookupField и PickerField, поэтому все описанное для этих интерфейсов в части работы с сущностями верно и для него. Исключением является список действий по умолчанию, добавляемых при определении компонента в XML: для SearchPickerField это действия lookup и open .

Атрибуты searchPickerField:

align | caption | captionProperty | datasource | description | editable | enable | filterMode | height | id | inputPrompt | metaClass | minSearchStringLength | nullName | optionsDatasource | property | required | requiredMessage | stylename | visible | width

Элементы searchPickerField:

actions | validator

4.5.2.1.25. Table

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

XML-имя компонента: table

Компонент реализован для блоков Web Client и Desktop Client.

Пример описания таблицы в XML-дескрипторе экрана:

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

Здесь в элементе dsContext определен источник данных collectionDatasource, который выбирает сущности Order с помощью JPQL запроса

select o from sales$Order o order by o.date. Для компонента table в элементе rows указывается используемый источник данных, а в элементе columns - какие атрибуты сущности, содержащейся в источнике данных, использовать в качестве колонок.

Элементы table:

  • rows - обязательный элемент, в атрибуте datasource которого необходимо объявить используемый таблицей источник данных.

    Для строк можно настроить отображение заголовков - задать каждой строке свой значок в дополнительной колонке слева. Для этого в контроллере экрана необходимо реализовать интерфейс Table.IconProvider и установить его таблице:

    @Inject
    protected Table customersTable;
    ...
        customersTable.setIconProvider(new Table.IconProvider() {
            @Nullable
            @Override
            public String getItemIcon(Entity entity) {
                CustomerGrade grade = ((Customer) 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;
                }
            }
        });

  • columns - обязательный элемент, определяет набор колонок таблицы.

    Каждая колонка описывается во вложенном элементе column со следующими атрибутами:

    • id − обязательный атрибут, содержит название атрибута сущности, выводимого в колонке. Может быть как непосредственным атрибутом сущности, находящейся в источнике данных, так и атрибутом связанной сущности - переход по графу объектов обозначается точкой. Например:

      <columns>
          <column id="date"/>
          <column id="customer"/>
          <column id="customer.name"/>
          <column id="customer.address.country"/>
      </columns>

    • caption − необязательный атрибут, содержит заголовок колонки. Если не задан, будет отображено локализованное название атрибута сущности.

    • collapsed − необязательный атрибут, при указании true колонка будет изначально скрыта. Пользователь может управлять отображением колонок с помощью меню, доступного по кнопке в правой верхней части таблицы, если атрибут columnControlVisible таблицы не false. По умолчанию collapsed имеет значение false.

    • width − необязательный атрибут, отвечает за изначальную ширину колонки.

    • align - необязательный атрибут, устанавливает выравнивание текста в ячейках данной колонки. Возможные значения: LEFT, RIGHT, CENTER. По умолчанию LEFT.

    • editable − необязательный атрибут, разрешает/запрещает редактирование данной колонки в редактируемой таблице. Чтобы колонка была редактируемой, атрибут editable всей таблицы (см. ниже) также должен быть установлен в true.

    • maxTextLength - необязательный атрибут, позволяет ограничивать количество символов в ячейке. При этом если разница между фактическим и допустимым количеством символов не превышает порог в 10 символов, "лишние" символы не скрываются. Для просмотра полной записи надо кликнуть на ее видимую часть. Пример колонки с ограничением в 5 символов:

    • link - установка атрибута в true позволяет отобразить в ячейке таблицы ссылку на экран просмотра экземпляра сущности (поддерживается только для Web Client). Атрибут link="true") может указываться и для колонок примитивных типов: в этом случае, при нажатии на ссылку будет открываться редактор основной сущности таблицы. Такой подход может применяться для упрощения навигации - пользователи смогут открывать редактор одним кликом по некоторому ключевому атрибуту.

    • linkScreen - позволяет указать идентификатор экрана, который будет открыт по нажатию на ссылку, включенную свойством link.

    • linkScreenOpenType - задает режим открытия экрана (THIS_TAB, NEW_TAB или DIALOG).

    • linkInvoke - позволяет заменить открытие окна на вызов метода контроллера.

    • Элемент column может содержать вложенный элемент formatter для представления значения атрибута в виде, отличном от стандартного для данного Datatype:

      <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, то пользователь может выделить несколько строк с помощью клавиатуры или мыши, удерживая клавиши Ctrl или Shift. По умолчанию режим множественного выделения отключен.

  • Атрибут sortable разрешает или запрещает сортировку в таблице. По умолчанию имеет значение true. Если сортировка разрешена, то при нажатии на название колонки справа от названия появляется значок /.

    При включенной с помощью элемента rowsCount (см. выше) страничной загрузке таблицы сортировка производится разными способами в зависимости от того, умещаются ли все записи на одной странице. Если умещаются, то сортировка производится в памяти, без обращений к БД. Если же страниц больше одной, то сортировка производится на базе данных путем отправки нового запроса с соответствующимORDER BY.

    Колонка таблицы может ссылаться на локальный атрибут или на связанную сущность. Например:

    <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. Если такой параметр не указан, то сортировка производится в памяти только в пределах текущей страницы.

  • Атрибут presentations управляет механизмом представлений. Значение по умолчанию равно false. Когда значение атрибута равно true, то в верхнем правом углу таблицы появляется значок . Механизм представлений реализован только для блока Web Client.

  • Установка атрибута columnControlVisible в false запрещает пользователю скрывать колонки с помощью меню, выпадающего при нажатия на кнопку в правой части шапки таблицы. Флажками в меню отмечаются отображаемые в данный момент колонки.

  • Установка атрибута reorderingAllowed в false запрещает пользователю менять местами колонки, перетаскивая их с помощью мыши.

  • Атрибут contextMenuEnabled разрешает или запрещает показывать контекстное меню. По умолчанию атрибут имеет значение true. В контекстном меню отображаются действия таблицы (если они есть), и пункт Системная информация, содержащий информацию о выбранной сущности (если у пользователя есть разрешение cuba.gui.showInfo, см. руководство по подсистеме безопасности).

  • Если атрибуту multiLineCells таблицы присвоить значение true, то ячейки, содержащие текст с переносами строк, будут отображать его в несколько строк. В таком режиме в веб клиенте для правильной работы полосы прокрутки все строки текущей страницы таблицы будут загружены веб-браузером сразу, без ленивой загрузки видимой части таблицы. По умолчанию атрибут имеет значение false.

  • Атрибут aggregatable включает режим агрегации строк таблицы. Поддерживаются следующие операции:

    • SUM - сумма

    • AVG - среднее значение

    • COUNT - количество

    • MIN - минимальное значение

    • MAX - максимальное значение

    Для агрегируемых колонок необходимо указать элемент aggregation с атрибутом type, задающим функцию агрегации. Агрегированные значения столбцов выводятся в дополнительной строке вверху таблицы. Пример описания таблицы с агрегацией:

    <table id="itemsTable" aggregatable="true">
        <columns>
            <column id="product"/>
            <column id="quantity"/>
            <column id="amount">
                <aggregation type="SUM"/>
            </column>
        </columns>
        <rows datasource="itemsDs"/>
    </table>

    Для отображения агрегированного значения в виде, отличном от стандартного для данного Datatype, для него можно указать Formatter:

    <column id="amount">
        <aggregation type="SUM">
            <formatter class="com.company.sample.MyFormatter"/>
        </aggregation>
    </column>

    В дополнение к операциям, перечисленным выше, можно задать собственную стратегию агрегации путем создания класса, реализующего интерфейс 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(total);
        }
        @Override
        public Class<String> getResultClass() {
            return String.class;
        }
    }

    AggregationInfo info = new AggregationInfo();
    info.setPropertyPath(metaPropertyPath);
    info.setStrategy(new TimeEntryAggregation());
    
    Table.Column column = weeklyReportsTable.getColumn(columnId);
    column.setAggregation(info);

  • Атрибут editable позволяет перевести таблицу в режим in-place редактирования ячеек. В этом режиме в колонках, имеющих атрибут editable = true, отображаются компоненты для редактирования значений атрибутов сущности, находящейся в источнике данных.

    Тип компонента для каждой редактируемой колонки выбирается автоматически на основании типа атрибута сущности. Например, для строковых и числовых атрибутов используется TextField, для Date - DateField, для перечислений - LookupField, для ссылок на другие сущности - PickerField.

    Для редактируемой колонки типа Date можно дополнительно указать атрибуты dateFormat или resolution аналогично описанным для DateField.

    Для редактируемой колонки, отображающей связанную сущность, можно дополнительно указать атрибуты optionsDatasource и captionProperty. При указании optionsDatasource вместо PickerField используется компонент LookupField.

    Произвольно настроить отображение ячеек, в том числе для редактирования содержимого, можно с помощью метода Table.addGeneratedColumn() - см. ниже.

Методы интерфейса Table:

  • getSelected(), getSingleSelected() - возвращают экземпляры сущностей, соответствующие выделенным в таблице строкам. Коллекцию можно получить вызовом метода getSelected(). Если ничего не выбрано, возвращается пустой набор. Если multiselect отключен, удобно пользоваться методом getSingleSelected(), возвращающим одну выбранную сущность или null, если ничего не выбрано.

  • Метод addGeneratedColumn() позволяет задать собственное представление данных в колонке. Он принимает два параметра: идентификатор колонки и реализацию интерфейсаTable.ColumnGenerator. Идентификатор может совпадать с одним из идентификаторов, указанных для колонок таблицы в XML-дескрипторе - в этом случае новая колонка вставляется вместо заданной в XML. Если идентификатор не совпадает ни с одной колонкой, создается новая справа.

    Метод generateCell() интерфейса Table.ColumnGenerator вызывается таблицей для каждой строки, и в него передается экземпляр сущности, отображаемой в данной строке. Метод generateCell() должен вернуть визуальный компонент, который и будет отображаться в ячейке.

    Пример использования:

    @Inject
    protected Table carsTable;
    
    @Inject
    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(entity), "colour");
                field.setOptionsDatasource(coloursDs);
                field.addLookupAction();
                field.addOpenAction();
                return field;
            }
        });
    }

    В данном случае в ячейках колонки colour таблицы отображается компонент LookupPickerField. Компонент должен сохранять свое значение в атрибут colour сущности, экземпляр которой отображается в данной строке. Для этого у таблицы методом getItemDatasource() запрашивается источник данных для текущего экземпляра сущности, и передается компоненту LookupPickerField.

    Если в ячейке необходимо отобразить просто динамически сформированный текст, вместо компонента Label используйте класс Table.PlainTextCell. Это упростит отрисовку и сделает таблицу быстрее.

    Если в метод addGeneratedColumn() передан идентификатор колонки, не объявленной в XML-дескрипторе, то может понадобиться установить заголовок новой колонки следующим образом:

    carsTable.getColumn("colour").setCaption("Colour");

  • Метод setClickListener() может избавить от необходимости добавлять генерируемые колонки с компонентами, если нужно нарисовать что-либо в ячейках и получать оповещения когда пользователь кликает на эти ячейки. Имплементация класса CellClickListener, передаваемая в данный метод, получает текущий экземпляр сущности и идентификатор колонки. Содержимое ячеек будет завернуто в элемент 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")) {
                    // 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;
            }
        });
    }

    Далее нужно определить заданные для строк и ячеек стили в теме приложения. Подробная информация о создании темы находится вРаздел 4.5.7, «Создание темы приложения». Для веб-клиента новые стили определяются в файле styles.scss. Имена стилей, заданные в контроллере, совместно с префиксами, обозначающими строку или колонку таблицы, образуют CSS-селекторы. Например:

    .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 для нее. Например:

    ordersTable.addPrintable("customer", new Table.Printable<Customer, String>() {
        @Override
        public String getValue(Customer customer) {
            return "Name: " + customer.getName;
        }
    });

    Метод getValue() интерфейса Table.Printable должен возвращать данные, которые будут находиться в ячейке таблицы. Это может быть не только строка - метод может возвращать значения других типов, например, числовые данные или даты, и они будут представлены в XLS-файле соответствующим образом.

    Если форматированный вывод в XLS необходим для генерируемой колонки, нужно использовать реализацию интерфейса Table.PrintableColumnGenerator, передавая ее методу addGeneratedColumn(). Значение для вывода в ячейку XLS-документа задается в методе getValue() этого интерфейса:

    ordersTable.addGeneratedColumn("product", new 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();
        }
    });

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

  • Метод setItemClickAction() позволяет задать действие, выполняемое при двойном клике на строке таблицы. Если такое действие не задано, при двойном клике таблица пытается найти среди своих действий подходящее в следующем порядке:

    • Действие, назначенное на клавишу Enter посредством свойства shortcut.

    • Действие с именем edit.

    • Действие с именем view.

    Если такое действие найдено и имеет свойство enabled = true, оно выполняется.

  • Метод setEnterPressAction() позволяет задать действие, выполняемое при нажатии клавиши Enter. Если такое действие не задано, таблица пытается найти среди своих действий подходящее в следующем порядке:

    • Действие, назначенное методом setItemClickAction().

    • Действие, назначенное на клавишу Enter посредством свойства shortcut.

    • Действие с именем edit.

    • Действие с именем view.

    Если такое действие найдено и имеет свойство enabled = true, оно выполняется.

Атрибуты table:

Элементы table:

Атрибуты column:

Элементы column:

Атрибуты rows:

4.5.2.1.26. TextArea

Текстовая область − многострочное текстовое поле для редактирования текста.

XML-имя компонента: textArea

Компонент TextArea реализован для блоков Web Client и Desktop Client.

TextArea в основном повторяет функциональность TextField, за исключением того, что ему нельзя установить datatype. То есть TextArea предназначен для работы только с текстом и строковыми атрибутами сущностей.

Компонент TextArea имеет следующие специфические атрибуты:

  • cols и rows задают количество строк и столбцов текста:

    <textArea id="textArea" cols="20" rows="5" caption="msg://name"/>

    Значения width и height имеют приоритет над значениями cols и rows.

  • resizable - при задании атрибуту значения true и установке количества строк, больших одной, появляется возможность изменять размеры компонента:

    <textArea id="textArea" resizable="true" caption="msg://name" rows="5"/>

Атрибуты textArea:

4.5.2.1.27. TextField

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

XML-имя компонента: textField

Компонент текстового поля реализован для блоков Web Client и Desktop Client.

  • Пример текстового поля с заголовком, взятым из пакета локализованных сообщений:

    <textField id="nameField" caption="msg://name"/>

    На рисунке ниже показан вид простого текстового поля.

  • Для создания текстового поля, связанного с данными, необходимо использовать атрибуты datasource и property.

    <dsContext>
    <datasource id="customerDs" class="com.sample.sales.entity.Customer" view="_local"/>
    </dsContext>
    <layout>
    <textField datasource="customerDs" property="name" caption="msg://name"/>

    Как видно из примера, в экране описывается источник данных customerDs для некоторой сущности Покупатель (Customer), имеющей атрибут name. В компоненте текстового поля в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в текстовом поле.

  • Если поле не связано с атрибутом сущности (то есть не указан источник данных и название атрибута), то можно указать тип данных с помощью атрибута datatype. Тип данных используется для форматирования значения поля. В качестве значения атрибута может быть указано любое имя типа данных, зарегистрированного в метаданных приложения - см. Раздел 4.2.2.3, «Datatype». Как правило, в TextField используются следующие типы данных:

    • decimal

    • double

    • int

    • long

    В качестве примера рассмотрим текстовое поле с типом данных Integer.

    <textField id="integerField" datatype="int" caption="msg://integerFieldName"/>

    Если в таком поле ввести значение, которое невозможно интерпретировать как целое число, то при потере фокуса полем будет выведено сообщение об ошибке и значение поля вернется на предыдущее:

  • Текстовому полю может быть назначен валидатор - класс, реализующий интерфейс Field.Validator. Валидатор позволяет дополнительно к datatype ограничить вводимую пользователем информацию. Например, для создания поля ввода положительных целых чисел нужно создать класс валидатора:

    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");
    }
    }

    и задать его для текстового поля с типом данных int в элементе validator:

    <textField id="integerField" datatype="int">
    <validator class="com.sample.sales.gui.PositiveIntegerValidator"/>
    </textField>

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

  • Если текстовое поле связано с атрибутом сущности (через datasource и property), и если для атрибута сущности в JPA-аннотации @Column указан параметр length, то TextField будет соответственно ограничивать максимальную длину вводимого текста.

    Если текстовое поле не связано с атрибутом, либо для него не определено значение length, либо это значение нужно переопределить, то для ограничения максимальной длины вводимого текста можно использовать атрибут maxLength. Значение "-1" означает отсутствие ограничения. Например:

    <textField id="shortTextField" maxLength="10"/>
  • По умолчанию текстовое поле отсекает пробелы в начале и конце введенной строки. То есть если пользователь ввел строку " aaa bbb " то значением поля, возвращаемым методом getValue() и сохраняемым в связанный атрибут сущности, будет строка "aaa bbb". Для того, чтобы отключить отсечение пробелов, используйте атрибут trim со значением false.

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

  • Текстовое поле всегда вместо введенной пустой строки возвращает null. Соответственно, при включенном атрибуте trim строка, состоящая из одних пробелов также превратится в null.

  • Атрибут inputPrompt задает строку, отображаемую в поле, если его значение равно null. Реализовано только для web клиента.

  • Метод setCursorPosition() используется для установки позиции курсора в указанный индекс (начинается с 0). После вызова метода поле принимает фокус ввода.

Атрибуты textField:

align | caption | datasource | datatype | description | editable | enable | height | id | inputPrompt | maxLength | property | required | requiredMessage | stylename | trim | visible | width

Элементы textField:

validator

4.5.2.1.28. TimeField

Поле для отображения и ввода времени.

XML-имя компонента: timeField.

Компонент TimeField реализован для блоков Web Client и Desktop Client.

  • Для создания поля даты, связанного с данными, необходимо использовать атрибуты datasource и property:

    <dsContext>
    <datasource id="orderDs" class="com.sample.sales.entity.Order" view="_local"/>
    </dsContext>
    <layout>
    <timeField datasource="orderDs" property="deliveryTime"/>

    Как видно из примера, в экране описывается источник данных orderDs для некоторой сущности Заказ (Order), имеющей атрибут deliveryTime. В компоненте ввода времени в атрибуте datasource указывается ссылка на источник данных, а в атрибуте property − название атрибута сущности, значение которого должно быть отображено в поле.

    Связанный атрибут сущности должен быть типа java.util.Date или java.sql.Time.

  • Формат отображения времени определяется типом данных time и задается в главном пакете локализованных сообщений в ключе timeFormat.

  • Формат отображения времени можно также задать в атрибуте timeFormat компонента. Это может быть как сама строка формата, так и ключ в пакете сообщений (с префиксом msg://).

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

    <timeField datasource="orderDs" property="createTs" showSeconds="true"/>

Атрибуты timeField: align | caption | editable | enable | datasource | description | height | id | property | required | requiredMessage | showSeconds | stylename | timeFormat | visible | width

Элементы timeField: validator

4.5.2.1.29. TokenList

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

XML-имя компонента: tokenList

Компонент реализован для блоков Web Client и Desktop Client.

Пример описания компонента TokenList в XML-дескрипторе экрана:

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

Здесь в элементе dsContext определен вложенный источник данных productsDs, содержащий коллекцию входящих в состав заказа продуктов. Кроме того, определен источник данных allProductsDs, содержащий коллекцию всех продуктов, имеющихся в базе данных. Компонент TokenList с идентификатором productsList отображает содержимое источника данных productsDs, а также позволяет изменять эту коллекцию, добавляя в него экземпляры из источника данных allProductsDs.

Атрибуты tokenList:

  • position - задает позиционирование раскрывающегося списка. Атрибут может принимать два значения: TOP, BOTTOM. По умолчанию TOP.

  • Атрибут inline задает отображение списка выбранных значений: вертикально или горизонтально. Значение true соответствует горизонтальному расположению, значение false − вертикальному. Так выглядит компонент с горизонтальным расположением значений:

  • simple - значение true позволяет убрать компонент выбора, оставляя только кнопку добавления. При нажатии на кнопку добавления сразу показывается экран списка экземпляров сущности, тип которой задан источником данных datasource. Идентификатор экрана выбора определяется по правилам, описанным для стандартного действия PickerField.LookupAction.

Элементы tokenList:

  • lookup − описатель компонента выбора значений.

    Атрибуты элемента lookup:

    • Атрибут lookup задает возможность выбора значений через экран выбора сущностей:

    • Атрибут lookupScreen задает идентификатор экрана для выбора значений в режиме lookup="true". Если данный атрибут не задан, то идентификатор экрана выбора определяется по правилам, описанным для стандартного действия PickerField.LookupAction.

    • Атрибут openType можно задать способ открытия экрана выбора, аналогично описанному для стандартного действия PickerField.LookupAction. По умолчанию - THIS_TAB.

    • Если значение атрибута multiselect установлено в true, то в мэп параметров экрана выбора в ключе MULTI_SELECT передается значение true. Этот признак можно использовать для установки в экране режима множественного выбора. Данный ключ определен в перечислении WindowParams, поэтому с ним удобно работать следующим образом:

      @Override
      public void init(Map<String, Object> params) {
          if (WindowParams.MULTI_SELECT.getBool(getContext())) {
              usersTable.setMultiSelect(true);
          }
      }

  • button − описатель кнопки добавления значений. Может содержать атрибуты caption и icon.

Все атрибуты tokenList:

Элементы tokenList:

Все атрибуты lookup:

Атрибуты button:

4.5.2.1.30. Tree

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

XML-имя компонента: tree

Компонент реализован для блоков Web Client и Desktop Client.

Для Tree в атрибуте datasource элемента treechildren должен быть указан hierarchicalDatasource. Объявление hierarchicalDatasource должно содержать атрибут hierarchyProperty - имя атрибута сущности, являющегося ссылкой на саму себя.

Пример описания компонента Tree в XML-дескрипторе экрана:

<dsContext>
<hierarchicalDatasource id="departmentsDs" 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>

В атрибуте captionProperty элемента treechildren можно задать имя свойства сущности, отображаемого в дереве. Если этот атрибут не определен, то будет отображаться имя экземпляра сущности.

Метод setItemClickAction() позволяет задать действие, которое будет выполнено при двойном клике по узлу дерева.

Атрибуты tree:

Элементы tree:

Атрибуты treechildren:

4.5.2.1.31. TreeTable

Компонент TreeTable − иерархическая таблица, отображающая в первой колонке древовидную структуру. Предназначена для работы с сущностями, которые содержат ссылки на самих себя. Это могут быть например, файловая система или организационная структура предприятия.

XML-имя компонента: treeTable

Компонент реализован для блоков Web Client и Desktop Client.

Для 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>
    <rows datasource="tasksDs"/>
</treeTable>

Функциональность TreeTable аналогична простой таблице Table.

Атрибуты treeTable:

Элементы treeTable:

Атрибуты column:

Элементы column:

Атрибуты rows:

4.5.2.1.32. TwinColumn

Компонент TwinColumn представляет собой сдвоенный список для множественного выбора опций. В левом списке содержатся доступные невыбранные значения, в правом списке содержатся выбранные значения. Пользователь выбирает значения, перенося их из левого в правый список и обратно с помощью двойного клика или соответствующих кнопок. Для каждого значения можно задать уникальный стиль отображения и пиктограмму.

XML-имя компонента: twinColumn

Компонент реализован только для блока Web Client.

Пример использования компонента twinColumn для выбора экземпляров сущности:

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

В данном случае компонент coloursField отобразит имена экземпляров сущности Colour, находящихся в источнике данных coloursDs, а его метод getValue() вернет коллекцию выбранных экземпляров сущности.

Атрибут addAllBtnEnabled задает отображение кнопок, позволяющих перемещать между списками все опции сразу.

Атрибут columns используется для задания количества символов в строке, а атрибут rows − для задания количества строк текста в каждом списке.

Для задания внешнего вида опций можно реализовать интерфейс TwinColumn.StyleProvider и возвращать название стиля и путь к пиктограмме в зависимости от конкретного экземпляра сущности, отображаемого в компоненте.

Список опций компонента TwinColumn может быть задан произвольно с помощью методов setOptionsList() и setOptionsMap(), аналогично описанному для компонента OptionsGroup.

Атрибуты twinColumn:

Элементы twinColumn:

4.5.2.2. Контейнеры

BoxLayout

ButtonsPanel

IFrame

GridLayout

GroupBoxLayout

ScrollBoxLayout

SplitPanel

TabSheet

4.5.2.2.1. BoxLayout

BoxLayout представляет собой контейнер с последовательным размещением компонентов.

Существует три типа BoxLayout, определяемых именем XML-элемента:

  • hbox − горизонтальное расположение компонентов.

    <hbox spacing="true" margin="true">
    <dateField datasource="orderDs" property="date"/>
    <lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
    <textField datasource="orderDs" property="amount"/>
    </hbox>
  • vbox − вертикальное расположение компонентов. vbox имеет 100% ширину по умолчанию.

    <vbox spacing="true" margin="true">
    <dateField datasource="orderDs" property="date"/>
    <lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
    <textField datasource="orderDs" property="amount"/>
    </vbox>
  • flowBox − горизонтальное расположение компонентов с переносом вниз. При недостатке места по горизонтали непомещающиеся компоненты будут перенесены "на следующую строку" (поведение аналогично Swing FlowLayout).

    <flowBox spacing="true" margin="true">
    <dateField datasource="orderDs" property="date"/>
    <lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
    <textField datasource="orderDs" property="amount"/>
    </flowBox>

В элементах hbox, vbox, flowBox могут быть использованы следующие XML-атрибуты:

4.5.2.2.2. ButtonsPanel

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

XML-имя компонента: buttonsPanel.

Пример описания ButtonsPanel в XML-дескрипторе экрана:

<table id="customersTable"
   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>

Элемент buttonsPanel можно разместить как внутри table, так и в произвольном месте экрана.

Если buttonsPanel находится внутри table, то она комбинируется с компонентом rowsCount таблицы, тем самым оптимально расходуя место по вертикали. Кроме того, в этом случае при открытии экрана выбора методом IFrame.openLookup() (например, из компонента PickerField) панель кнопок скрывается.

Атрибут alwaysVisible служит для отключения скрытия панели в экране выбора при его открытии методом IFrame.openLookup(). Если значение атрибута равно true, то панель с кнопками не скрывается. По умолчанию значение атрибута равно false.

Атрибуты buttonsPanel:

4.5.2.2.3. GridLayout

GridLayout - контейнер, располагающий компоненты по сетке.

XML-имя компонента: grid.

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

<grid spacing="true">
<columns count="4"/>
<rows>
    <row>
        <label value="Date"/>
        <dateField datasource="orderDs" property="date"/>
        <label value="Customer"/>
        <lookupField datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
    </row>
    <row>
        <label value="Amount"/>
        <textField datasource="orderDs" property="amount"/>
    </row>
</rows>
</grid>

Элементы grid:

  • columns - обязательный элемент, описывает колонки сетки. Должен либо иметь атрибут count, либо вложенные элементы column для каждой колонки.

    В простейшем случае достаточно задать число колонок в атрибуте count. Тогда, если ширина всего контейнера явно задана в пикселах или процентах, незанятое место будет распределяться между колонками равными долями.

    Для распределения незанятого места неравными долями необходимо определить для каждой колонки элемент column и задать для него атрибут flex.

    Пример сетки, в которой вторая и четвертая колонки занимают все лишнее место по горизонтали, причем четвертая колонка забирает себе в три раза больше лишнего места:

    <grid spacing="true" width="100%">
    <columns>
        <column/>
        <column flex="1"/>
        <column/>
        <column flex="3"/>
    </columns>
    <rows>
        <row>
            <label value="Date"/>
            <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>

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

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

  • rows − обязательный элемент, содержит последовательность строк. Каждая строка определяется в своем элементе row.

    Элемент row может содержать атрибут flex, аналогичный описанному для column, но влияющий на распределение лишнего места по вертикали при заданной общей высоте сетки.

    Элемент row должен содержать элементы компонентов, отображаемых в ячейках данной строки сетки. Число компонентов в одной строке не должно превышать заданного количества колонок, но может быть меньше.

Любой компонент, находящийся в контейнере grid, может иметь атрибуты colspan и rowspan. Эти атрибуты задают соответственно сколько колонок и строк будет занимать данный компонент. Например, так можно растянуть поле Field3 на три колонки:

<grid spacing="true">
<columns count="4"/>
<rows>
    <row>
        <label value="Field1"/>
        <textField/>
        <label value="Field2"/>
        <textField/>
    </row>
    <row>
        <label value="Field3"/>
        <textField colspan="3" width="100%"/>
    </row>
</rows>
</grid>

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

Атрибуты grid:

Элементы grid:

columns    
rows    

Атрибуты columns:

count    

Атрибуты column:

flex    

Атрибуты row:

flex    
visible    
4.5.2.2.4. GroupBoxLayout

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

XML-имя компонента: groupBox.

Пример описание контейнера в XML-дескрипторе экрана:

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

Атрибуты groupBox:

  • caption - заголовок группы.

  • orientation - задает направление расположения вложенных компонентов − horizontal или vertical. По умолчанию vertical.

  • collapsable − значение true позволяет пользователю скрывать содержимое компонента с помощью значков /.

  • collapsed − если указано значение true, то содержимое компонента будет свернуто сразу после открытия экрана. Используется совместно с collapsable="true".

    Пример свернутого GroupBox:

Контейнер groupBox по умолчанию имеет ширину 100% аналогично vbox.

Все атрибуты groupBox:

4.5.2.2.5. IFrame

Элемент iframe предназначен для включения в экран фрейма.

Атрибуты:

  • src − путь к XML-дескриптору фрейма.

  • screen - идентификатор фрейма в screens.xml (если фрейм зарегистрирован).

Должен быть указан один из этих атрибутов. Если указано оба, фрейм будет загружен из явно указанного в src файла.

Другие атрибуты iframe:

4.5.2.2.6. ScrollBoxLayout

ScrollBoxLayout − контейнер, который позволяет прокручивать свое содержимое.

XML-имя компонента: scrollBox

Пример описание контейнера с прокруткой в XML-дескрипторе экрана:

<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>
  • С помощью атрибута orientation можно задавать направление расположения вложенных компонентов − horizontal или vertical. По умолчанию vertical.

  • Атрибут scrollBars позволяет настраивать полосы прокрутки. Может принимать значения horizontal, vertical - для прокрутки по горизонтали и вертикали соответственно, both - для прокрутки во всех направлениях. Установка значения none запрещает прокрутку в любом направлении

Вложенные в scrollBox компоненты должны иметь фиксированные размеры или размеры по умолчанию. Нельзя устанавливать height="100%" или width="100%".

В то же время scrollBox не может вычислять свои собственные размеры по содержимому. Ему нужно либо указать абсолютные размеры, либо растянуть в родительском контейнере, установив height="100%" и width="100%".

Атрибуты scrollBox:

4.5.2.2.7. SplitPanel

SplitPanel − контейнер, разбитый на две области, размер которых по горизонтали либо вертикали можно менять путем перемещения разделителя.

XML-имя компонента: split.

Пример описания панели с разделителем в XML-дескрипторе экрана:

<split orientation="horizontal" pos="30" width="100%" 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 обязательно должны находиться два вложенных контейнера или компонента, которые и будут расположены по обе стороны разделителя.

Атрибуты split:

  • orientation - задает ориентацию расположения компонентов. horizontal - вложенные компоненты располагаются горизонтально, vertical - вертикально.

  • pos - целое число, определяющее процентное соотношение размера первой области по отношению ко второй. Например, pos="30" означает соотношение областей 30/70. По умолчанию соотношение областей составляет 50/50.

Все атрибуты split:

id pos   
height width   
orientation    
4.5.2.2.8. TabSheet

Контейнер TabSheet - это панель с вкладками (tabs). В один момент времени отображается содержимое только одной вкладки.

XML-имя компонента: tabSheet.

Пример описания панели с вкладками в XML-дескрипторе экрана:

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

Компонент tabSheet должен иметь вложенные элементы tab, описывающие вкладки. Каждая вкладка является контейнером с вертикальным расположением компонентов, аналогичным vbox.

Атрибуты элемента tab:

  • id - идентификатор вкладки. Следует отметить, что вкладка не является компонентом, и данный идентификатор используется только в рамках TabSheet для работы с ней из кода контроллера.

  • caption - заголовок вкладки.

  • lazy - задает отложенную загрузку содержимого вкладки.

    При открытии экрана lazy-вкладки не загружают свое содержимое, что приводит к созданию меньшего количества компонентов в памяти. Компоненты вкладки загружаются только в тот момент, когда пользователь выбирает данную вкладку. Кроме того, если на lazy-вкладке расположены визуальные компоненты, связанные с источником данных, содержащим JPQL запрос, то этот запрос также не выполняется. В результате экран открывается быстрее, а данные загружаются только в тот момент, когда пользователь действительно хочет их увидеть, выбирая данную вкладку.

    Следует иметь в виду, что компоненты, расположенные на lazy-вкладке, не существуют в момент открытия экрана. Поэтому их нельзя инжектировать в контроллер, и нельзя получить вызовом getComponent() в методе init() контроллера. Обратиться к компонентам lazy-вкладки можно только после того, как пользователь на нее переключился. Этот момент можно отловить с помощью слушателя TabSheet.TabChangeListener, например:

    @Inject
    private TabSheet tabsheet;
    
    private boolean detailsInitialized, historyInitialized;
    
    @Override
    public void init(Map<String, Object> params) {
    tabsheet.addListener(
            new TabSheet.TabChangeListener() {
                @Override
                public void tabChanged(TabSheet.Tab newTab) {
                    if ("detailsTab".equals(newTab.getName()))
                        initDetails();
                    else if ("historyTab".equals(newTab.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;
    }

    По умолчанию вкладки не являются lazy, а значит, загружают свое содержимое в момент открытия экрана.

  • detachable - значение true в десктоп-реализации экрана дает возможность отсоединять вкладку в отдельное окно. Это позволяет, например, размещать части UI приложения на разных мониторах. Отделяемая вкладка имеет специальную кнопку в заголовке:

Атрибуты tabSheet:

height visible   
id width   
stylename    

Все атрибуты элемента tab:

4.5.2.3. Разное

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

4.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.DateFormatter" format="yyyy-MM-dd HH:mm:ss"/>
</column>

Кроме того, класс DateFormatter распознает также атрибут type, который может принимать значения DATE или DATETIME. В этом случае форматирование производится с помощью механизма Datatype по строке формата dateFormat или dateTimeFormat соответственно. Например:

<column id="endDate">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" type="DATE"/>
</column>

Если formatter реализован внутренним классом, то он должен быть объявлен с модификатором static, а его имя для загрузки отделяется символом "$", например:

<formatter class="com.sample.sales.gui.OrderBrowse$CurrencyFormatter"/>

Formatter можно назначить компоненту не только в XML-дескрипторе экрана, но и программно, передавая экземпляр formatter'а в метод setFormatter() компонента.

Пример объявления собственного formatter'а и использования его для форматирования значения колонки таблицы:

public class CurrencyFormatter implements Formatter<BigDecimal> {

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(currencyFormatter);
}
4.5.2.3.2. Presentation

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

Возможности:

  • Сохранение представлений под уникальными именами

  • Редактирование и удаление представлений

  • Быстрое переключение между представлениями

  • Задание представления по умолчанию, которое будет применяться при открытии экрана с компонентом

  • Автосохранение настроек отображения в активном представлении

  • Глобальные представления, которые доступны всем пользователям системы

Классы и интерфейсы

Для применения представлений класс компонента должен реализовывать интерфейс com.haulmont.cuba.gui.components.Component.HasPresentations. В платформе такими компонентами являются:

Presentation − POJO объект представления.

Presentations содержит список представлений компонента и набор методов по работе с ними. Основные методы:

  • getCurrent() − возвращает текущее активное представление или null, если представление не выбрано

  • setCurrent(Presentation p) − устанавливает активное представление

  • getSettings(Presentation p) − возвращает XML-элемент настроек отображения для указанного представления

  • setSettings(Presentation p, Element e) − модифицирует настройки отображения для указанного представления

  • getPresentation(Object id) − возвращает представление по его идентификатору

  • getPresentations() − возвращает список идентификаторов представлений для данного компонента

  • commit() − сохраняет представления в базу данных

PresentationsImpl − реализация интерфейса Presentations.

PresentationsChangeListener − интерфейс слушателя изменений представлений.

Для создания, изменения, удаления глобальных представлений пользователь должен иметь права cuba.gui.presentations.global. Подробнее см. руководство Платформа CUBA. Подсистема безопасности.

4.5.2.3.3. Timer

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

Компонент реализован для блоков Web Client и Desktop Client. Для веб клиента реализация таймеров основана на опросе сервера из веб-браузера, для десктоп клиента - на 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 ...
<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();
}

Таймер можно инжектировать в поле контроллера, либо получить методом Window.getTimer(). Управлять активностью таймера можно с помощью его методов start() и stop(). Для уже активного таймера вызов start() игнорируется. После остановки таймера методом stop() его можно снова запустить методом start().

Задать обработчик событий от таймера можно с помощью реализации интерфейса Timer.TimerListener:

<timers>
<timer id="helloTimer" delay="5000"/>
</timers>
@Inject
private Timer helloTimer;

@Override
public void init(Map<String, Object> params) {
helloTimer.addTimerListener(new Timer.TimerListener() {
    @Override
    public void onTimer(Timer timer) {
        showNotification("Hello", NotificationType.HUMANIZED);
    }

    @Override
    public void onStopTimer(Timer timer) {
        showNotification("Timer is stopped", NotificationType.HUMANIZED);
    }
});
helloTimer.start();
}

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

@Inject
private ComponentsFactory componentsFactory;

@Override
public void init(Map<String, Object> params) {
Timer helloTimer = componentsFactory.createTimer();
helloTimer.setDelay(5000);
helloTimer.setRepeating(true);
helloTimer.addTimerListener(new Timer.TimerListener() {
    @Override
    public void onTimer(Timer timer) {
        showNotification("Hello", NotificationType.HUMANIZED);
    }

    @Override
    public void onStopTimer(Timer timer) {
        showNotification("Timer is stopped", NotificationType.HUMANIZED);
    }
});
helloTimer.start();

addTimer(helloTimer);
}
4.5.2.3.4. Validator

Валидатор предназначен для проверки значения, введенного в визуальном компоненте.

Следует отличать валидацию от проверки типа данных. Если для некоторого компонента, например TextField, задан тип, отличный от строкового (это происходит при связывании с атрибутом сущности или назначении datatype), то компонент не позволяет ввести значение, не удовлетворяющее этому типу - при потере фокуса или нажатии Enter компонент отобразит предыдущее значение.

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

В XML-дескрипторе экрана валидатор для компонента может быть задан во вложенном элементе validator. Возможные атрибуты элемента validator:

  • script − путь к скрипту Groovy, осуществляющему валидацию.

  • class − имя класса Java, реализующего интерфейс Field.Validator.

  • Groovy-валидатор и стандартные классы Java-валидаторов, расположенные в пакете com.haulmont.cuba.gui.components.validators поддерживают атрибут message − сообщение, выводимое пользователю в случае ошибки валидации. Атрибут должен содержать сообщение или ключ в пакете сообщений экрана, например:

    <validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
               message="msg://validationError"
               pattern="\d{3}"/>

    # messages.properties
    validationError = Input error

Выбор механизма валидации осуществляется следующим образом:

  • Если не указано значение атрибута script, и сам элемент validator не содержит текста выражения Groovy, то в качестве валидатора используется класс, указанный в атрибуте class.

  • Если элемент validator содержит текст, то он будет использован как выражение Groovy и выполнен с помощью Scripting.

  • В противном случае с помощью Scripting будет выполнен скрипт Groovy, указанный в атрибуте script.

В выражение или скрипт Groovy будет передана одна переменная value, содержащая значение, введенное в визуальном компоненте. Выражение или скрипт должны вернуть boolean значение: true − valid, false − not valid.

Если в качестве валидатора используется класс Java, то он должен иметь либо дефолтный конструктор без параметров, либо конструктор со следующим набором параметров:

  • org.dom4j.Element, String - в этот конструктор будут переданы XML-элемент валидатора и имя пакета сообщений экрана.

  • org.dom4j.Element - в этот конструктор будет передан XML-элемент валидатора.

Если валидатор реализован внутренним классом, то он должен быть объявлен с модификатором static, а его имя для загрузки отделяется символом "$", например:

<validator class="com.sample.sales.gui.AddressEdit$ZipValidator"/>

Платформа уже содержит несколько реализаций наиболее часто используемых валидаторов (см. пакет com.haulmont.cuba.gui.components.validators), которые можно применять в проектах:

  • DateValidator

  • DoubleValidator

  • EmailValidator

  • IntegerValidator

  • LongValidator

  • PatternValidator

  • ScriptValidator

Валидатор-класс можно назначить компоненту не только в XML-дескрипторе экрана, но и программно, передавая экземпляр валидатора в метод addValidator() компонента.

Пример создания класса валидатора почтового индекса:

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");
    }
}

Использование валидатора почтового индекса и стандартного валидатора по шаблону в полях компонента FieldGroup:

<fieldGroup>
    <field id="zip" required="true">
         <validator class="com.company.sample.gui.ZipValidator"/>
    </field>
    <field id="imei">
        <validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
               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("paramIsRequiredButEmpty"));
            }
        }
    });
}

4.5.2.4. XML-атрибуты компонентов

align

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

Возможные значения:

  • TOP_RIGHT

  • TOP_LEFT

  • TOP_CENTER

  • MIDDLE_RIGHT

  • MIDDLE_LEFT

  • MIDDLE_CENTER

  • BOTTOM_RIGHT

  • BOTTOM_LEFT

  • BOTTOM_CENTER

caption

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

Значением атрибута должна быть либо строка сообщения, либо ключ в пакете сообщений. В случае ключа значение должно начинаться с префикса msg://

Способы задания ключа:

  • Короткий ключ − при этом сообщение ищется в пакете, заданном для данного экрана:

    caption="msg://infoFieldCaption"
  • Полный ключ, с заданием пакета:

    caption="msg://com.haulmont.refapp.gui.app/infoFieldCaption"
captionProperty

Задает имя атрибута сущности, отображаемого компонентом. Используется только для сущностей, находящихся в источнике данных (например заданном для LookupField свойством optionsDatasource).

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

clickAction

Атрибут содержит описание действия, которое будет выполнено при клике в ячейке или в поле (для компонента FieldGroup). Возможны два типа действий:

  • open − открывает для сущности, отображаемой в ячейке, экран редактирования с указанным именем, например: clickAction="open:sec$User.edit". Имя сущности отображается в виде ссылки:

  • invoke − вызывает метод контроллера экрана с указанным именем, например: clickAction="invoke:onClick". Метод должен иметь единственный параметр типа Object, в который будет передан экземпляр сущности, отображаемой в ячейке.

colspan

Указывает, сколько колонок сетки должен занять компонент (по умолчанию 1).

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

datasource

Предназначен для задания источника данных, описанного в секции dsContext XML-дескриптора экрана.

При указании атрибута datasource для компонента, реализующего интерфейс DatasourceComponent, необходимо также задать атрибут property.

description

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

editable

Атрибут, указывающий на возможность редактирования содержимого компонента (не путать с enable).

Возможные значения − true, false. По умолчанию true.

На возможность редактирования содержимого для компонента, связанного с данными (наследника DatasourceComponent или ListComponent), влияет также подсистема безопасности. Если по данным подсистемы безопасности данный компонент должен быть недоступен для редактирования, значение атрибута editable не принимается во внимание.

enable

Атрибут компонента, устанавливающий его состояние: доступен, недоступен.

Если компонент недоступен, то он не принимает фокус ввода. Недоступность контейнера приводит к тому, что все его компоненты также становятся недоступными. Возможные значения − true, false. По умолчанию все компоненты доступны.

expand

Атрибут контейнера для управления его внутренней компоновкой.

Задает компонент внутри контейнера, который необходимо расширить на все доступное пространство в направлении размещения компонентов. Для контейнера с вертикальным размещением устанавливает компоненту 100% высоту, для контейнера с горизонтальным размещением - 100% ширину. Кроме того, при изменении размера контейнера изменять размер будет именно этот компонент.

height

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

Может быть задана в пикселях либо в процентах от высоты вышестоящего контейнера. Например: 100px, 100%, 50. Если единица измерения не указана, подразумевается высота в пикселях.

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

При выборе значения AUTO или -1px для компонента устанавливается высота по умолчанию, для контейнера высота определяется по содержимому, то есть суммарной высотой вложенных компонентов.

icon

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

Значением атрибута должен быть путь к файлу пиктограммы относительно каталога темы. Например:

icon="icons/create.png"

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

icon="msg://addIcon"

В веб клиенте с темой Halo (или производной от нее) вместо файлов можно использовать элементы шрифта Font Awesome. Для этого достаточно указать константу из класса com.vaadin.server.FontAwesome с префиксом font-icon: например:

icon="font-icon:BOOK"
id

Идентификатор компонента.

Рекомендуется формировать значение по правилам Java-идентификаторов и использовать camelСase, например, userGrid, filterPanel. Может быть указан для любого компонента и должен быть уникальным в пределах экрана.

inputPrompt

Атрибут inputPrompt задает строку, отображаемую в поле, если его значение равно null.

Атрибут используется для компонентов TextField, LookupField, LookupPickerField, SearchPickerField только в web клиенте.

margin

Атрибут margin устанавливает наличие отступа между внешними границами и содержимым контейнера.

Может иметь 2 вида значений:

  • margin="true" − установить отступ со всех сторон сразу

  • margin="true,false,true,false" − установить отступ только сверху и снизу (формат значения "сверху,справа,снизу,слева")

По умолчанию отступы отсутствуют.

nullName

Идентификатор опции, выбор которой будет равносилен установке значения в null.

Атрибут используется для компонентов LookupField, LookupPickerField, SearchPickerField.

Пример для компонента LookupField, установка значения атрибута в XML-дескрипторе:

<lookupField datasource="orderDs"
         property="customer"
         nullName="(none)"
         optionsDatasource="customersDs" width="200px"/>

Пример для компонента LookupField, установка значения атрибута в контроллере:

<lookupField id="customerLookupField" optionsDatasource="customersDs"
         width="200px" datasource="orderDs" property="customer"/>
customerLookupField.setNullOption("<null>");
optionsDatasource

Задает имя источника данных, используемого для формирования списка опций.

Совместно с optionsDatasource может использоваться атрибут captionProperty.

property

Атрибут компонента, реализующего интерфейс DatasourceComponent.

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

Используется всегда совместно с атрибутом datasource.

required

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

Возможные значения атрибута − true, false. По умолчанию false.

Совместно с required может использоваться атрибут requiredMessage.

requiredMessage

XML-атрибут, используемый совместно с атрибутом required. Позволяет установить сообщение, выводимое пользователю в случае нарушения требования required.

Атрибут должен содержать ключ сообщения в пакете, например: requiredMessage="msg://infoTextField.requiredMessage"

rowspan

Указывает, сколько строк сетки должен занять компонент (по умолчанию 1).

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

spacing

Атрибут spacing устанавливает наличие отступов между компонентами внутри контейнера.

Возможные значения − true, false.

По умолчанию отступы отсутствуют.

stylename

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

visible

Атрибут, устанавливающий видимость компонента. Возможные значения − true, false.

Если контейнер невидим, не видны и все его компоненты. По умолчанию все компоненты видимы.

width

Атрибут, устанавливающий ширину компонента.

Значение может быть задано в пикселях или в процентах от ширины вышестоящего контейнера. Например: 100px, 100%, 50. Если единица измерения не указана, подразумевается ширина в пикселях. Простановка значения в % означает, что компонент по ширине займет соответствующую часть пространства, предоставляемого контейнером более высокого уровня.

При выборе значения AUTO или -1px для компонента устанавливается ширина по умолчанию, для контейнера ширина определяется по содержимому, то есть суммарной шириной вложенных компонентов.

4.5.3. Источники данных

Источники данных обеспечивают работу связанных с данными (data-aware) компонентов.

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

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

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

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

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

  • При необходимости прочитать или записать значение атрибута сущности в коде предпочтительнее использовать источник данных, а не компонент. Рассмотрим пример чтения атрибута:

    @Inject
    private FieldGroup fieldGroup;
    
    @Inject
    private Datasource<Order> orderDs;
    
    public void init(Map<String, Object> params) {
        Customer customer;
        // Get customer from component
        customer = (Customer) fieldGroup.getFieldValue("customer");
        // Get customer from datasource
        customer = orderDs.getItem().getCustomer();
    }

    Как видно из примера, работа со значениями атрибутов сущностей через компонент требует приведения типа и, в случае FieldGroup, указания имени атрибута в виде строки. В то же время, получив методом getItem() из источника данных хранящийся в нем экземпляр, можно напрямую читать и изменять значения его атрибутов.

Как правило, визуальный компонент привязывается к атрибуту, непосредственно принадлежащему сущности, находящейся в источнике данных. В приведенном выше примере компонент привязан к атрибуту customer сущности Order.

Можно также привязать компонент к атрибуту связанной сущности, например к customer.name. В этом случае компонент будет корректно отображать значение атрибута name, но при его изменении пользователем слушатели источника данных вызваны не будут, и изменения не будут сохранены. Поэтому привязывать компонент к атрибутам второго и более порядка имеет смысл только для отображения, например в Label, колонке Table или установив для TextField свойство editable = false.

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

Рассмотрим основные интерфейсы источников.

Рисунок 24. Интерфейсы источников данных

Интерфейсы источников данных

  • 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 − специфический источник, предназначенный для работы с динамическими атрибутами сущностей.

Как правило, источники данных объявляются декларативно в секции dsContext дескриптора экрана.

4.5.3.1. Создание источников данных

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

4.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"/>
      <collectionDatasource id="repairsDs" property="repairs"/>
  </datasource>

  <collectionDatasource id="colorsDs" class="com.haulmont.sample.entity.Color" view="_local">
      <query>
          select c from sample$Color c order by c.name
      </query>
  </collectionDatasource>
</dsContext>

Здесь источник carDs содержит один экземпляр сущности Car, а вложенные в него allocationsDs и repairsDs содержат коллекции связанных сущностей из атрибутов Car.driverAllocations и Car.repairs соответственно. Экземпляр Car вместе со связанными сущностями проставляется в источник данных извне. Если данный экран является экраном редактирования, то это происходит автоматически при открытии экрана. Источник данных colorsDs содержит коллекцию экземпляров сущности Color, загружаемую самим источником по указанному JPQL-запросу с представлением _local.

Рассмотрим схему XML.

dsContext - корневой элемент.

Элементы dsContext:

  • datasource - определяет источник данных, содержащий единственный экземпляр сущности.

    Атрибуты:

    • 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>) params.get("customers");
        for (Customer entity : entities) {
            customersDs.includeItem(entity);
        }
        customersDs.refresh();
      }
    • softDeletion - значение false отключает режим мягкого удаления при загрузке сущностей, т.е. будут загружены также и удаленные экземпляры. По умолчанию true.

    Элементы collectionDatasource:

    • query - запрос для загрузки сущностей

  • groupDatasource - полностью аналогичен collectionDatasource, но создает реализацию источника данных, пригодную для использования совместно с компонентом GroupTable .

  • hierarchicalDatasource - аналогичен collectionDatasource, и создает реализацию источника данных, пригодную для использования совместно с компонентами Tree и TreeTable .

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

Класс реализации источника выбирается неявно на основе имени элемента XML и, как было сказано выше, взаимного расположения элементов. Однако если необходимо применить нестандартный источник данных, его класс может быть явно указан в атрибуте datasourceClass.

4.5.3.1.2. Программное создание

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

Экземпляр DsBuilder параметризуется цепочкой вызовов его методов в стиле текучего (fluent) интерфейса. Если установлены параметры master и property, то в результате будет создан NestedDatasource, в противном случае - Datasource или CollectionDatasource.

Пример:

CollectionDatasource ds = new DsBuilder(getDsContext())
      .setJavaClass(Order.class)
      .setViewName(View.LOCAL)
      .setId("ordersDs")
      .buildCollectionDatasource();
4.5.3.1.3. Собственные классы реализации

Как правило, нестандартная реализация источника данных требуется для изменения процесса загрузки коллекции сущностей. При создании класса такого источника рекомендуется унаследовать его от CollectionDatasourceImpl, либо от GroupDatasourceImpl или HierarchicalDatasourceImpl, и переопределить метод loadData().

Пример:

public class MyDatasource extends CollectionDatasourceImpl<SomeEntity, UUID> {

  private SomeService someService = AppBeans.get(SomeService.NAME);

  @Override
  protected void loadData(Map<String, Object> params) {
      detachListener(data.values());
      data.clear();

      for (SomeEntity entity : someService.getEntities()) {
          data.put(entity.getId(), entity);
          attachListener(entity);
      }
  }
}

Здесь data - поле базового класса, хранящее коллекцию загруженных экземпляров. Методы базового класса detachListener() и attachListener() управляют назначением на загруженные сущности слушателя, который оповещает источник данных об изменениях в полях экземпляров.

Для создания нестандартного источника данных декларативным способом необходимо указать класс в атрибуте datasourceClass элемента XML. При программном создании через DsBuilder класс источника указывается вызовом setDsClass().

4.5.3.2. Запросы в CollectionDatasourceImpl

Класс CollectionDatasourceImpl и его наследники GroupDatasourceImpl, HierarchicalDatasourceImpl являются стандартной реализацией источников данных, работающих с коллекциями независимых экземпляров сущностей. Эти источники загружают данные через DataManager, отправляя на Middleware запрос на языке JPQL. Далее рассматриваются особенности формирования таких запросов.

4.5.3.2.1. Возвращаемые значения

Запрос должен возвращать сущности того типа, который указан при создании источника данных. Тип сущности при декларативном создании указывается в атрибуте class элемента XML, при создании через DsBuilder - в методе setJavaClass() или setMetaClass().

Кроме того, тип объекта в предложении from запроса должен соответствовать типу источника. Это необходимо для проведения автоматических трансформаций запроса при наложении ограничений безопасности и др.

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

select c from sales$Customer c

Примеры недопустимых для источника типа Customer запросов:

select c.id, c.name from sales$Customer c /* неверно - возвращает отдельные поля, а не весь объект Customer */

select o.customer from sales$Order o /* неверно - тип from (Order) отличается от типа результата (Customer) */
4.5.3.2.2. Параметры запроса

JPQL-запрос в источнике данных может содержать параметры нескольких видов. Вид параметра определяется по префиксу имени параметра. Префиксом является часть имени до знака "$". Интерпретация имени после "$" рассматривается ниже.

  • Префикс ds.

    Значением параметра являются данные другого источника данных, зарегистрированного в этом же DsContext. Например:

    <collectionDatasource id="customersDs" class="com.sample.sales.entity.Customer" view="_local">
      <query>
          select c from sales$Customer c
      </query>
    </collectionDatasource>
    
    <collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
      <query>
          select o from sales$Order o where o.customer.id = :ds$customersDs
      </query>
    </collectionDatasource>

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

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

    Обратите внимание, что в примере запроса с параметром левой частью оператора сравнения является значение идентификатора o.customer.id, а правой - экземпляр Customer, содержащийся в источнике customersDs. Такое сравнение допустимо, так как при выполнении запроса на Middleware реализация интерфейса Query , присваивая значения параметрам запроса, автоматически подставляет ID сущности вместо переданного экземпляра сущности.

    В имени параметра после префикса и имени источника может быть также указан путь по графу сущностей к атрибуту, из которого нужно взять значение, например:

    <query>
      select o from sales$Order o where o.customer.id = :ds$customersDs.id
    </query>

    или

    <query>
      select o from sales$Order o where o.tagName = :ds$customersDs.group.tagName
    </query>
  • Префикс custom.

    Значение параметра будет взято из объекта Map<String, Object>, переданного в метод refresh() источника данных. Например:

    <collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
      <query>
          select o from sales$Order o where o.number = :custom$number
      </query>
    </collectionDatasource>
    Map<String, Object> params = new HashMap<>();
    params.put("number", "1");
    ordersDs.refresh(params);

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

  • Префикс param.

    Значение параметра будет взято из объекта Map<String, Object>, переданного при открытии экрана в метод init() контроллера.

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

  • Префикс component.

    Значением параметра будет текущее значение визуального компонента, путь к которому указан в имени параметра. Например:

    <query>
      select o from sales$Order o where o.number = :component$filter.orderNumberField
    </query>

    Путь к компоненту должен включать все вложенные фреймы.

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

  • Префикс session.

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

    Значение извлекается методом UserSession.getAttribute(), поэтому поддерживаются также предопределенные имена атрибутов сессии:

    • userId - ID текущего зарегистрированного или замещенного пользователя;

    • userLogin - логин текущего зарегистрированного или замещенного пользователя в нижнем регистре.

    Пример:

    <query>
      select o from sales$Order o where o.createdBy = :session$userLogin
    </query>

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

Если значение параметра не найдено по правилам, задаваемым префиксом, для данного параметра устанавливается значение null. То есть если, например, в запросе указан параметр с именем param$some_name, а в мэп параметров экрана нет ключа some_name, то для параметра param$some_name устанавливается значение null.

4.5.3.2.3. Фильтр запроса

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

Простейший способ обеспечения такой возможности - подключение к источнику данных специального визуального компонента Filter.

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

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

  • filter - корневой элемент фильтра. Может непосредственно содержать только одно условие.

    • and, or - логические условия, могут содержать любое количество других условий и предложений.

    • c - предложение на JPQL, которое добавляется в секцию where. Содержит только текст и опционально атрибут join, значение которого будет добавлено в соответствующее место запроса, если добавляется данное предложение where.

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

Пример:

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

В данном случае если в метод refresh() источника данных переданы параметры state и initiator, а в визуальном компоненте barCodeFilterField установлено некоторое значение, то итоговый запрос примет вид:

select distinct d from app$GeneralDoc d, app$DocRole dr
where
(
  (dr.doc.id = d.id and d.processState = :custom$state)
  and
  (d.barCode like :component$barCodeFilterField)
)
or
(dr.doc.id = d.id and dr.user.id = :custom$initiator)

Если же, к примеру, компонент barCodeFilterField пуст, а в refresh() передан только параметр initiator, то запрос получится следующим:

select distinct d from app$GeneralDoc d, app$DocRole dr
where
(dr.doc.id = d.id and dr.user.id = :custom$initiator)

Не используйте ds-параметры в фильтрах запросов. Они предназначены для связывания источников данных и обрабатываются специальным образом.

4.5.3.2.4. Поиск подстроки без учета регистра

В источниках данных можно использовать особенность выполнения JPQL-запросов, описанную для интерфейса Query уровня Middleware: для удобного формирования условия поиска без учета регистра символов и по любой части строки можно использовать префикс (?i). Однако, в связи с тем, что значение параметра обычно передается неявно, имеются следующие отличия:

  • Префикс (?i) нужно указывать не в значении, а перед именем параметра.

  • Значение параметра будет автоматически переведено в нижний регистр.

  • Если в значении параметра отсутствуют символы %, то они будут добавлены в начало и конец.

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

select c from sales$Customer c where c.name like :(?i)component$customerNameField

В данном случае значение параметра, взятое из компонента customerNameField, будет переведено в нижний регистр и обрамлено символами %, а затем в базе данных будет выполнен SQL запрос с условием вида lower(C.NAME) like ?

Следует иметь в виду, что при таком поиске индекс, созданный в БД по полю NAME, не используется.

4.5.3.3. Слушатели источников данных

Слушатели источников данных (datasource listeners) позволяют получать оповещения об изменении состояния источников данных и экземпляров сущностей, в них находящихся.

Для регистрации слушателей используются методы Datasource.addListener(), Datasource.removeListener(). Пример регистрации слушателя в контроллере экрана:

@Inject
private Datasource<Customer> customerDs;
...
public void init(Map<String, Object> params) {
  ...
  customerDs.addListener(new DatasourceListener<Customer>() {
      // listener methods implementation
  });
}

Существует два интерфейса слушателей источников данных: DatasourceListener и CollectionDatasourceListener. Первый можно использовать для регистрации в любых источниках данных, второй - только в реализующих CollectionDatasource. Как правило, на практике требуется получать не все оповещения от слушателя, а только некоторые. Поэтому удобно вместо реализации самих интерфейсов слушателей использовать классы-адаптеры DsListenerAdapter и CollectionDsListenerAdapter, содержащие пустые реализации всех методов соответствующих интерфейсов.

Рассмотрим методы DatasourceListener:

  • valueChanged() - объявление этого метода наследуется от базового интерфейса ValueListener. Данный метод слушателя вызывается, если изменилось значение какого-либо атрибута сущности, находящейся в данный момент в источнике. В метод передается сам измененный экземпляр, имя измененного атрибута, старое и новое значение.

    Оповещение valueChanged() можно использовать для действий в ответ на изменение пользователем сущности из UI, то есть редактирования полей ввода. В следующем примере гипотетический метод updateSettings() будет вызван при изменении значения атрибута active, и в него будет передано новое значение этого атрибута:

    @Inject
    private Datasource<Customer> customerDs;
    
    public void init(Map<String, Object> params) {
      ...
      customerDs.addListener(new DsListenerAdapter<Customer>() {
          @Override
          public void valueChanged(Customer source, String property, Object prevValue, Object value) {
              if ("active".equals(property)) {
                  boolean active = BooleanUtils.isTrue((Boolean) value); // converting null to false
                  updateSettings(active);
              }
          }
      });
    }
  • itemChanged() - вызывается при смене выбранного экземпляра, возвращаемого методом getItem().

    Для Datasource это происходит при установке другого экземпляра (или null) методом setItem().

    Для CollectionDatasource данное оповещение вызывается, когда в связанном визуальном компоненте меняется выделенный элемент. Например, это может быть выделенная строка таблицы, элемент дерева, или выделенный элемент выпадающего списка.

    Пример использования оповещения itemChanged()для управления состоянием действия таблицы:
    @Inject
    protected CollectionDatasource<Customer, UUID> customersDs;
    
    @Named("customersTable.remove")
    protected RemoveAction removeAction;
    
    public void init(Map<String, Object> params) {
      ...
      customersDs.addListener(new DsListenerAdapter<Customer>() {
          @Override
          public void itemChanged(Datasource<Customer> ds, Customer prevItem, Customer item) {
              removeAction.setEnabled(canCustomerBeDeleted(item));
          }
      });
    }
  • stateChanged() - вызывается при изменении состояния источника данных. Источник данных может находиться в одном из трех состояний, соответствующих перечислению Datasource.State:

    • NOT_INITIALIZED - источник только что создан.

    • INVALID - создан весь DsContext , к которому относится данный источник.

    • VALID - источник данных в рабочем состоянии: Datasource содержит экземпляр сущности или null, CollectionDatasource - коллекцию экземпляров или пустую коллекцию.

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

    @Inject
    protected CollectionPropertyDatasourceImpl<CategoryAttribute, UUID> categoryAttrsDs;
    
    categoryAttrsDs.addListener(new DsListenerAdapter<CategoryAttribute>() {
      @Override
      public void stateChanged(Datasource ds, Datasource.State prevState, Datasource.State state) {
          if (state != Datasource.State.VALID) return;
          initDataTypeColumn();
          initDefaultValueColumn();
      }
    });

Интерфейс CollectionDatasourceListener добавляет еще один метод:

  • collectionChanged() - вызывается при изменении коллекции сущностей, хранящейся в источнике данных. В метод передается тип изменения: REFRESH, CLEAR, ADD, REMOVE, UPDATE.

    Пример слушателя, вызывающего пересчет стоимости поездки при изменении адреса остановки (сущность Stop) или количества остановок:

    protected class StopDsListener extends CollectionDsListenerAdapter<Stop> {
      @Override
      public void valueChanged(Stop source, String property, Object prevValue, Object value) {
          // existing stop address changed
          if ("address".equals(property)) {
              fireRouteChanged();
          }
      }
    
      @Override
      public void collectionChanged(CollectionDatasource ds, Operation operation) {
          // stop was added or removed
          fireRouteChanged();
      }
    
      private void fireRouteChanged() {
          // journey route has changed, need to recalculate price, journey time, pickup time delay etc.
      }
    }

4.5.3.4. DsContext

Все созданные декларативно источники данных регистрируются в объекте DsContext экрана. Ссылку на DsContext можно получить методом getDsContext() контроллера экрана, либо инжекцией в поле класса.

DsContext решает следующие задачи:

  1. Позволяет организовать зависимости между источниками данных, когда при навигации по одному источнику (т.е. при изменении "текущего" экземпляра методом setItem()) обновляется связанный источник. Такие зависимости дают возможность в экранах легко организовывать master-detail связи между визуальными компонентами.

    Зависимости между источниками организуются с помощью параметров запросов с префиксом ds$.

  2. Позволяет собрать все измененные экземпляры сущностей и отправить их на Middleware в одном вызове DataManager.commit(), т.е. сохранить в базе данных в одной транзакции.

    В качестве примера предположим, что некоторый экран позволяет редактировать экземпляр сущности Order и коллекцию принадлежащих ему экземпляров OrderLine. Экземпляр Order находится в Datasource, коллекция OrderLine - во вложенном CollectionDatasource, созданном по атрибуту Order.lines. Допустим, пользователь изменил какой-то атрибут Order и создал новый экземпляр OrderLine. Тогда при коммите экрана в DataManager будут одновременно отправлены два экземпляра - измененный Order и новый OrderLine. Далее, они вместе попадут в один персистентный контекст и при коммите транзакции сохранятся в БД. Разумеется, экземпляр OrderLine содержится также в коллекции Order.lines, но если не передавать его в персистентный контекст независимо, то потребуется установка каскадности сохранения между Order и OrderLines на уровне ORM. Жесткие отношения каскадности на уровне ORM иногда вызывают нежелательные последствия в неожиданных местах, поэтому лучше их избегать, что и обеспечивает описываемый механизм DsContext.

    В результате коммита DsContext получает от Middleware набор сохраненных экземпляров (в случае оптимистической блокировки у них, как минимум, увеличено значение атрибута version), и устанавливает эти экземпляры в источниках данных взамен устаревших. Это позволяет сразу после коммита работать со свежими экземплярами без необходимости лишнего обновления источников данных, связанного с запросами к Middleware и базе данных.

  3. Объявляет слушателя DsContext.CommitListener, позволяющего получать оповещения перед коммитом измененных экземпляров и после него. Перед коммитом можно дополнить коллекцию отправляемых в DataManager на Middleware экземпляров, тем самым обеспечив сохранение в той же транзакции произвольных сущностей. После коммита можно получить коллекцию вернувшихся из DataManager сохраненных экземпляров.

    Данный механизм необходим, если некоторые сущности, с которыми работает экран, находятся не под управлением источников данных, а создаются и изменяются непосредственно в коде контроллера. Например, визуальный компонент FileUploadField после загрузки файла создает новый экземпляр сущности FileDescriptor, который можно сохранить вместе с другими сущностями экрана именно таким способом - добавив в CommitContext в методе DsContext.CommitListener.beforeCommit().

    DsContext.CommitListener имеет адаптер DsContext.CommitListenerAdapter, который удобно использовать при необходимости определить только один метод.

    В следующем примере новый экземпляр Customer будет отправлен на Middleware и сохранен в БД вместе с остальными измененными сущностями экрана при его коммите:

    protected Customer customer;
    
    protected void createNewCustomer() {
      customer = new Customer();
      customer.setName("John Doe");
    }
    
    public void init(Map<String, Object> params) {
      getDsContext().addListener(new DsContext.CommitListenerAdapter() {
          @Override
          public void beforeCommit(CommitContext context) {
              if (customer != null)
                  context.getCommitInstances().add(customer);
          }
      });
    }

4.5.3.5. DataSupplier

DataSupplier - интерфейс, через который источники данных обращаются к Middleware для загрузки и сохранения сущностей. Его стандартная реализация просто делегирует выполнение DataManager. Экран может задать свою реализацию интерфейса DataSupplier в атрибуте dataSupplier элемента window. Собственная реализация может, например, вызывать дополнительный блок Middleware для загрузки данных экрана из другой базы данных.

Ссылку на DataSupplier можно получить либо инжекцией в контроллер экрана, либо через экземпляры DsContext или Datasource. В обоих случаях возвращается или стандартная, или собственная реализация интерфейса (если таковая определена).

4.5.4. Действия. Интерфейс Action

Action − интерфейс, абстрагирующий действие (другими словами, некоторую функцию) от визуального компонента. Он особенно полезен в случаях, когда одно и то же действие может быть вызвано из разных визуальных компонентов. Кроме того, данный интерфейс позволяет снабдить действие дополнительными свойствами, такими как название, признаки доступности и видимости, и другими.

Рассмотрим методы интерфейса Action:

  • actionPerform() - вызывается визуальным компонентом, связанным с данным действием. В метод передается экземпляр вызвавшего компонента.

  • getId() - возвращает идентификатор данного действия. Идентификатор обычно устанавливается конструктором класса, реализующего Action, и не меняется на протяжении жизни созданного объекта действия.

  • методы получения и установки свойств caption, description, shortcut, icon, enabled, visible. Все эти свойства обычно используется связанными визуальными компонентами для установки собственных одноименных свойств.

  • addPropertyChangeListener(), removePropertyChangeListener() - подключение слушателей, реагирующих на изменение вышеупомянутых свойств. Слушатель получает уведомление типа java.beans.PropertyChangeEvent, в котором содержится имя измененного свойства, его старое и новое значение.

  • refreshState() - метод, который может быть реализован в конкретном классе действия для инициализации вышеупомянутых свойств в соответствии с какими-либо внешними факторами, например правами пользователя. Вызывается обычно в конструкторах имплементирующих классов или из связанных визуальных компонентов.

  • addOwner(), removeOwner(), getOwner(), getOwners() - методы для управления связью действия с визуальными компонентами.

Для реализации действий рекомендуется использовать декларативное создание действий, либо наследоваться от класса AbstractAction. Кроме того, существует набор стандартных действий, применимых для работы с таблицами и компонентами выбора. От стандартных действий также можно наследоваться для модификации их поведения или перехвата событий.

Визуальные компоненты, связанные с действием, могут быть двух типов:

  • Визуальный компонент, содержащий одно действие, реализует интерфейс Component.ActionOwner. Это Button и LinkButton.

    Связь компонента с действием осуществляется путем вызова метода ActionOwner.setAction() компонента. В этот момент компонент заменяет свои свойства на соответствующие свойства действия (подробнее см. описание компонентов).

  • Визуальный компонент, содержащий несколько действий, реализует интерфейс Component.ActionsHolder. Это Window, IFrame, Table и ее наследники, Tree, PopupButton, PickerField, LookupPickerField.

    Действия добавляются компоненту вызовом метода ActionsHolder.addAction(). Реализация этого метода в компоненте проверяет, нет ли уже в нем действия с таким же идентификатором. Если есть, то имеющееся действие будет заменено на новое переданное. Поэтому можно, например, декларировать стандартное действие в дескрипторе экрана, а затем в контроллере создать новое с переопределенными методами и добавить компоненту.

4.5.4.1. Декларативное создание действий

В XML-дескрипторе экрана для любого компонента, реализующего интерфейс Component.ActionsHolder, в том числе для всего экрана или фрейма, может быть задан набор действий. Делается это в элементе actions, который содержит вложенные элементы action.

Элемент action может иметь следующие атрибуты:

  • id − идентификатор, должен быть уникален в рамках данного компонента ActionsHolder.

  • caption - название действия.

  • description - описание действия.

  • enable - признак доступности действия (true / false).

  • icon - значок действия.

  • invoke - имя вызываемого метода контроллера. Метод должен быть public, не возвращать результата и либо не иметь аргументов, либо иметь один аргумент типа Component. Если метод имеет аргумент Component, то при вызове в него будет передан экземпляр визуального компонента, запустившего данное действие.

  • shortcut - комбинация клавиш для вызова. Возможные модификаторы - ALT, CTRL, SHIFT - отделяются символом "-". Например: ALT-CTRL-C.

  • visible - признак видимости действия (true / false).

Рассмотрим примеры декларативного объявления действий.

  • Объявление действий на уровне экрана:

    <window ...>
      <dsContext/>
    
      <actions>
          <action id="sayHelloAction" caption="msg://sayHello" shortcut="ALT-T" invoke="sayHello"/>
      </actions>
    
      <layout>
          <button action="sayHelloAction"/>
      </layout>
    </window>
    // controller
    
    public void sayHello(Component component) {
      showNotification("Hello!", NotificationType.TRAY);
    }

    Здесь объявляется действие с идентификатором sayHelloAction и названием из пакета сообщений. С этим действием связывается кнопка, заголовок которой будет установлен в название действия. Действие вызовет метод sayHello() контроллера при нажатии на кнопку, а также при нажатии комбинации клавиш ALT-T, если в данный момент экран принимает фокус ввода.

  • Объявление действий для PopupButton:

    <popupButton caption="Say something">
     <actions>
        <action id="helloAction" caption="Say hello" invoke="sayHello"/>
        <action id="goodbyeAction" caption="Say goodbye" invoke="sayGoodbye"/>
     </actions>
    </popupButton>
  • Объявление действий для Table:

    <table id="usersTable" width="100%">
      <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 объявлены действия copy и changePassw, вызывающие соответствующие методы контроллера. Для этих действий указан также атрибут trackSelection="true", в результате чего действие и связанная с ним кнопка становятся недоступными, если в таблице не выбрана ни одна строка. Это удобно, если действие предназначено для выполнения над текущей выбранной строкой таблицы.

    Для действий create и edit можно указать дополнительный атрибут openType для указания режима открытия экрана редактирования, как описано для метода setOpenType() класса CreateAction.

  • Объявление действий для PickerField:

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

    В данном примере для компонента PickerField объявлено стандартное действие lookup и действие show, вызывающее метод showColour() контроллера. Так как в кнопках PickerField, отображающих действия, используются значки, а не надписи, атрибут caption явно установлен в пустую строку, иначе названием действия и заголовком кнопки стал бы идентификатор действия. Атрибут description позволяет отображать всплывающую подсказку при наведении мыши на кнопку действия.

Ссылки на любые декларативно объявленные действия можно получить в контроллере экрана либо непосредственно путем инжекции, либо из компонентов, реализующих интерфейс Component.ActionsHolder. Это может понадобиться для программной установки свойств действия. Например:

@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);
}

4.5.4.2. Стандартные действия

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

Существует два вида стандартных действий:

4.5.4.2.1. Стандартные действия с коллекцией

Для наследников ListComponent (это Table, GroupTable, TreeTable и Tree) набор стандартных действий определен в перечислении ListActionType, классы их реализации находятся в пакете com.haulmont.cuba.gui.components.actions.

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

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

Рассмотрим их подробнее.

4.5.4.2.1.1. CreateAction

CreateAction - действие с идентификатором create. Предназначено для создания нового экземляра сущности и открытия экрана редактирования для этого экземпляра. Если экран редактирования успешно закоммитил новый экземпляр в базу данных, то CreateAction добавляет этот новый экземпляр в источник данных таблицы и делает его выбранным.

В классе CreateAction определены следующие специфические методы:

  • setOpenType() - позволяет задать режим открытия экрана редактирования новой сущности. По умолчанию экран открывается в режиме THIS_TAB.

    Так как довольно часто требуется открывать экраны редактирования в другом режиме (как правило, DIALOG), при декларативном создании действия create в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:

    <table id="usersTable">
      <actions>
          <action id="create" openType="DIALOG"/>

  • setWindowId() - позволяет задать идентификатор экрана редактирования сущности. По умолчанию используется экран {имя_сущности}.edit, например sales$Customer.edit.

  • setWindowParams() - позволяет задать параметры экрана редактирования, передаваемые в его метод init().

  • setInitialValues() - позволяет задать начальные значения атрибутов создаваемой сущности. Принимает объект Map, в котором ключами являются имена атрибутов, а значениями - значения атрибутов. Например:

    Map<String, Object> values = new HashMap<>();
    values.put("type", CarType.PASSENGER);
    carCreateAction.setInitialValues(values);

    Пример использования setInitialValues() приведен также в разделе рецептов разработки.

  • afterCommit() - вызывается действием после того, как экран редактирования успешно закоммитил новую сущность и был закрыт. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

  • setAfterCommitHandler() - позволяет задать обработчик, который будет вызван после того, как экран редактирования успешно закоммитил новую сущность и был закрыт. Данный обработчик можно использовать вместо переопределения метода afterCommit(), тем самым избавившись от необходимости создания наследника действия. Например:

    @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(), тем самым избавившись от необходимости создания наследника действия.

4.5.4.2.1.2. EditAction

EditAction - действие с идентификатором edit. Открывает экран редактирования для выбранного экземпляра сущности. Если экран редактирования успешно закоммитил экземпляр в базу данных, то EditAction обновляет этот экземпляр в источнике данных таблицы.

В классе EditAction определены следующие специфические методы:

  • setOpenType() - позволяет задать режим открытия экрана редактирования сущности. По умолчанию экран открывается в режиме THIS_TAB.

    Так как довольно часто требуется открывать экраны редактирования в другом режиме (как правило, DIALOG), при декларативном создании действия edit в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:

    <table id="usersTable">
      <actions>
          <action id="edit" openType="DIALOG"/>

  • setWindowId() - позволяет задать идентификатор экрана редактирования сущности. По умолчанию используется экран {имя_сущности}.edit, например sales$Customer.edit.

  • setWindowParams() - позволяет задать параметры экрана редактирования, передаваемые в его метод init().

  • afterCommit() - вызывается действием после того, как экран редактирования успешно закоммитил сущность и был закрыт. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

  • setAfterCommitHandler() - позволяет задать обработчик, который будет вызван после того, как экран редактирования успешно закоммитил новую сущность и был закрыт. Данный обработчик можно использовать вместо переопределения метода afterCommit(), тем самым избавившись от необходимости создания наследника действия. Например:

    @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);
            }
        });
    }

  • afterWindowClosed() - вызывается действием в последнюю очередь после закрытия экрана редактирования, независимо от того, была ли закоммичена редактируемая сущность. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

  • setAfterWindowClosedHandler() - позволяет задать обработчик, который будет вызван после закрытия экрана редактирования, независимо от того, была ли закоммичена новая сущность или нет. Данный обработчик можно использовать вместо переопределения метода afterWindowClosed(), тем самым избавившись от необходимости создания наследника действия.

4.5.4.2.1.3. RemoveAction

RemoveAction - действие с идентификатором remove. Предназначено для удаления выбранного экземпляра сущности.

В классе RemoveAction определены следующие специфические методы:

  • setAutocommit() - позволяет управлять моментом удаления сущности из базы данных. По умолчанию после срабатывания действия и удаления сущности из источника данных у источника вызывается метод commit(), в результате чего сущность удаляется из базы данных. Cвойство autocommit можно установить в false либо методом setAutocommit(), либо соответствующим параметром конструктора. В результате после удаления сущности из источника данных для подтверждения удаления потребуется явно вызвать метод commit() источника данных.

    Значение autocommit не влияет на работу источников данных в режиме Datasource.CommitMode.PARENT, то есть тех, которые обеспечивают редактирование композиционных сущностей.

  • setConfirmationMessage() - позволяет задать текст сообщения в диалоге подтверждения удаления.

  • setConfirmationTitle() - позволяет задать заголовок диалога подтверждения удаления.

  • afterRemove() - вызывается действием после успешного удаления сущности. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

  • setAfterRemoveHandler() позволяет задать обработчик, который будет вызван после успешного удаления сущности. Данный обработчик можно использовать вместо переопределения метода afterWindowClosed(), тем самым избавившись от необходимости создания наследника действия. Например:

    @Named("customersTable.remove")
    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);
            }
        });
    }

4.5.4.2.1.4. RefreshAction

RefreshAction - действие с идентификатором refresh. Предназначено для обновления (перезагрузки) коллекции сущностей. При срабатывании вызывает метод refresh() источника данных, с которым связан компонент.

В классе RefreshAction определены следующие специфические методы:

  • setRefreshParams() - позволяет задать параметры, передаваемые в метод CollectionDatasource.refresh(), для использования внутри запроса. По умолчанию никакие параметры не передаются.

4.5.4.2.1.5. AddAction

AddAction - действие с идентификатором add. Предназначено для выбора существующего экземпляра сущности и добавления его в коллекцию. При срабатывании открывает экран выбора сущностей.

В классе AddAction определены следующие специфические методы:

  • setOpenType() - позволяет задать режим открытия экрана выбора сущности. По умолчанию экран открывается в режиме THIS_TAB.

    Так как довольно часто требуется открывать экраны выбора в другом режиме (как правило, DIALOG), при декларативном создании действия add в элементе action можно указать атрибут openType с нужным значением. Это избавляет от необходимости получать ссылку на действие в контроллере и программно устанавливать данное свойство. Например:

    <table id="usersTable">
    <actions>
      <action id="add" openType="DIALOG"/>
  • setWindowId() - позволяет задать идентификатор экрана выбора сущности. По умолчанию используется экран {имя_сущности}.lookup, например sales$Customer.lookup. Если такого экрана не существует, то делается попытка открыть экран {имя_сущности}.browse, например sales$Customer.browse.

  • setWindowParams() - позволяет задать параметры экрана выбора, передаваемые в его метод init().

  • setHandler() - позволяет задать объект, реализующий интерфейс Window.Lookup.Handler, передаваемый в экран выбора. По умолчанию используется объект класса AddAction.DefaultHandler.

4.5.4.2.1.6. ExcludeAction

ExcludeAction - действие с идентификатором exclude. Позволяет исключать экземпляры сущности из коллекции, не удаляя их из базы данных. Класс данного действия является наследником RemoveAction, однако при срабатывании вызывает у CollectionDatasource не removeItem(), а excludeItem(). Кроме того, для вложенных источников данных ExcludeAction разрывает связь с родительской сущностью, поэтому с помощью данного действия можно организовать редактирование ассоциации one-to-many.

В классе ExcludeAction в дополнение к RemoveAction определены следующие специфические методы:

  • setConfirm() - показывать ли диалог подтверждения удаления. Это свойство можно также установить через конструктор действия. По умолчанию установлено в false.

4.5.4.2.1.7. ExcelAction

ExcelAction - действие с идентификатором excel. Предназначено для экспорта данных таблицы в формат XLS и выгрузки соответствующего файла. Данное действие можно связать только с компонентами Table, GroupTable и TreeTable.

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

  • display - реализация интерфейса ExportDisplay для выгрузки файла. По умолчанию используется стандартная реализация.

  • parameterized - при установке в true действие отображает специальное окно с идентификатором excelExport, позволяющее пользователю выбрать колонки таблицы для экспорта.

4.5.4.2.2. Стандартные действия поля выбора

Для компонентов PickerField, LookupPickerField и SearchPickerField набор стандартных действий определен в перечисленииPickerField.ActionType. Реализации являются внутренними классами интерфейса PickerField.

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

<searchPickerField optionsDatasource="coloursDs"
                 datasource="carDs" property="colour">
  <actions>
      <action id="clear"/>
      <action id="lookup"/>
      <action id="open"/>
  </actions>
</searchPickerField>

4.5.4.2.2.1. LookupAction

LookupAction - действие с идентификатором lookup. Предназначено для выбора экземпляра сущности и установки его в качестве значения компонента. При срабатывании открывает экран выбора сущностей.

В классе LookupAction определены следующие специфические методы:

  • setLookupScreenOpenType() - позволяет задать режим открытия экрана выбора сущности. По умолчанию экран открывается в режиме THIS_TAB.

  • setLookupScreenDialogParams() - позволяет задать свойства дилогового окна при открытия экрана выбора сущности в режиме DIALOG (см. предыдущий метод). На другие режимы влияния не оказывает.

  • setLookupScreen() - позволяет задать идентификатор экрана выбора сущности. По умолчанию используется экран {имя_сущности}.lookup, например sales$Customer.lookup. Если такого экрана не существует, то делается попытка открыть экран {имя_сущности}.browse, например sales$Customer.browse.

  • setLookupScreenParams() - позволяет задать параметры экрана выбора, передаваемые в его метод init().

  • afterSelect() - вызывается действием после того, как выбранный экземпляр установлен в качестве значения компонента. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

  • afterCloseLookup() - вызывается действием в последнюю очередь после закрытия экрана выбора, независимо от того, был сделан выбор или нет. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

4.5.4.2.2.2. ClearAction

ClearAction - действие с идентификатором clear. Предназначено для очистки (то есть установки в null) текущего значения компонента.

4.5.4.2.2.3. OpenAction

OpenAction - действие с идентификатором open. Предназначено для открытия экрана редактирования экземпляра сущности, являющегося текущим значением компонента.

В классе OpenAction определены следующие специфические методы:

  • setEditScreenOpenType() - позволяет задать режим открытия экрана редактирования сущности. По умолчанию экран открывается в режиме THIS_TAB.

  • setEditScreenDialogParams() - позволяет задать свойства дилогового окна при открытия экрана редактирования в режиме DIALOG (см. предыдущий метод). На другие режимы влияния не оказывает.

  • setEditScreen() - позволяет задать идентификатор экрана редактирования сущности. По умолчанию используется экран {имя_сущности}.edit, например sales$Customer.edit.

  • setEditScreenParams() - позволяет задать параметры экрана редактирования, передаваемые в его метод init().

  • afterWindowClosed() - вызывается действием после закрытия экрана редактирования. Данный метод не имеет реализации и может быть переопределен в наследниках для реакции на это событие.

4.5.4.3. BaseAction

BaseAction - базовый класс реализации действий. От него рекомендуется наследовать собственные нестандартные действия, если возможностей декларативного создания действий не хватает.

При создании конкретного класса действия необходимо определить метод actionPerform() и передать в конструктор BaseAction идентификатор действия. Можно также переопределить любые методы получения свойств действия: getCaption(), getDescription(), getIcon(), getShortcut(), isEnabled(), isVisible(). Стандартные реализации этих методов возвращают значения, установленные соответствующими set-методами. Исключение составляет метод getCaption(): если название действия явно не установлено методом setCaption(), то он обращается в пакет локализованных сообщений с именем, соответствующим пакету класса действия, и возвращает сообщение с ключом, равным идентификатору действия. Если сообщения с таким ключом нет, то возвращается сам ключ, то есть идентификатор действия.

BaseAction может изменять свои свойства enabled и visible в соответствии с разрешениями пользователя и текущим контекстом.

BaseAction видим (visible), если:

  • метод setVisible(false) не вызывался;

  • для действия не установлено UI разрешение hide.

Действие разрешено (enabled), если:

  • метод setEnabled(false) не вызывался;

  • для действия не установлено UI разрешений hide или read-only;

  • метод isPermitted() возвращает true;

  • метод isApplicable() возвращает true.

Примеры использования:

  • Действие кнопки:

    @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);
            }
        });
    }

    В данном случае кнопка helloBtn получит в качестве заголовка строку, находящуюся в пакете сообщений с ключом hello. Для того, чтобы получить название кнопки каким-либо иным способом, можно переопределить метод 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);
            }
        });
    
        box.add(pickerField);
    }

    Здесь анонимный класс-наследник BaseAction используется для задания действия кнопки поля выбора. Заголовок кнопки не отображается, вместо него используется значок и описание, всплывающее при наведении курсора мыши.

  • Действие таблицы:

    @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() {
            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;
        }
    }

    Здесь объявлен класс HelloAction, экземпляр которого добавляется в список действий таблицы. Действие разрешено пользователям, имеющим специфическое разрешение myapp.allow-greeting, и только когда выбрана одна строка таблицы. Последнее условие реализуется с помощью свойства target действия, которое автоматически устанавливается когда действие добавляется в ListComponent (Table или Tree).

  • Если необходимо действие, которое доступно, когда выделены одна или более строк таблицы, удобно воспользоваться наследником BaseAction - классом ItemTrackingAction, который добавляет стандартную реализацию метода isApplicable():

    @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);
            }
        });
    }

4.5.5. Диалоговые окна и уведомления

Для вывода сообщений пользователю можно использовать диалоговые окна и уведомления.

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

4.5.5.1. Диалоговые окна

Диалоговые окна вызываются методами showMessageDialog() и showOptionDialog() интерфейса IFrame. Этот интерфейс реализуется контроллером экрана, поэтому данные методы можно вызывать напрямую в коде контроллера.

  • showMessageDialog() предназначен для отображения сообщения. Метод принимает следующие параметры:

    • title - заголовок диалогового окна.

    • message - сообщение. В случае HTML-типа (см. ниже) в сообщении можно использовать теги HTML для форматирования. При использовании HTML обязательно экранируйте данных из БД во избежание code injection в веб-клиенте. В не-HTML сообщениях можно использовать символы \n для переноса строки.

    • messageType - тип сообщения. Возможные типы:

      • CONFIRMATION, CONFIRMATION_HTML - диалог подтверждения.

      • WARNING, WARNING_HTML - диалог преупреждения.

      Различие типов сообщений отражается только в пользовательском интерфейсе десктоп-приложений.

    Пример вызова диалога:

    showMessageDialog("Warning", "Something is wrong", MessageType.WARNING);
  • showOptionDialog() предназначен для отображения сообщения и кнопок для выбора пользователем. Метод в дополнение к параметрам, описанным для showMessageDialog(), принимает массив или список действий. Для каждого действия в диалоге создается кнопка, при нажатии на которую пользователем диалог закрывается и вызывается метод actionPerform() данного действия.

    В качестве кнопок со стандартными названиями и значками удобно использовать анонимные классы, унаследованные от DialogAction. Поддерживаются пять видов действий, определяемых перечислением DialogAction.Type: OK, CANCEL, YES, NO, CLOSE. Названия соответствующих кнопок извлекаются из главного пакета локализованных сообщений.

    Пример вызова диалога с кнопками Да и Нет и с заголовком и сообщением, взятыми из пакета локализованных сообщений текущего экрана:

    showOptionDialog(
          getMessage("confirmCopy.title"),
          getMessage("confirmCopy.msg"),
          MessageType.CONFIRMATION,
          new Action[]{
                  new DialogAction(DialogAction.Type.YES) {
                      public void actionPerform(Component component) {
                          copySettings();
                      }
                  },
                  new DialogAction(DialogAction.Type.NO)
          }
    );

4.5.5.2. Уведомления

Уведомления вызываются методом showNotification() интерфейса IFrame. Этот интерфейс реализуется контроллером экрана, поэтому данный метод можно вызывать напрямую в коде контроллера.

Метод showNotification() принимает следующие параметры:

  • caption - текст уведомления. В случае HTML-типа (см. ниже) в сообщении можно использовать теги HTML для форматирования. При использовании HTML обязательно экранируйте данных из БД во избежание code injection в веб-клиенте. В не-HTML сообщениях можно использовать символы \n для переноса строки.

  • description - опциональное описание, которое будет отображено ниже caption. Также можно использовать символы \n или HTML-форматирование.

  • type - тип уведомления. Возможные типы:

    • TRAY, TRAY_HTML - уведомление показывается в правом нижнем углу приложения и исчезает автоматически.

    • HUMANIZED, HUMANIZED_HTML - стадартное уведомление в центре экрана, исчезает автоматически.

    • WARNING, WARNING_HTML - предупреждение. Исчезает при клике пользователя.

    • ERROR, ERROR_HTML - уведомление об ошибке. Исчезает при клике пользователя.

Примеры вызова уведомлений:

showNotification(getMessage("selectBook.text"), NotificationType.HUMANIZED);

showNotification("Validation error", "<b>Date</b> is incorrect", NotificationType.TRAY_HTML);

4.5.6. Фоновые задачи

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

4.5.6.1. Использование фоновых задач

  1. Задача описывается как наследник абстрактного класса BackgroundTask. В конструктор задачи необходимо передать ссылку на контроллер экрана, с которым будет связана задача, и значение таймаута ее выполнения.

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

    Собственно действия, выполняемые задачей, реализуются в методе 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() {
          // 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();
}

Подробная информация о назначении методов приведена в JavaDocs классов BackgroundTask, TaskLifeCycle, BackgroundTaskHandler.

Ниже приведены моменты, на которые следует обратить внимание:

  • BackgroundTask<T, V> − параметризованный класс:

    • T − тип объектов, показывающих прогресс задачи. Объекты этого типа передаются в метод progress() задачи при вызове TaskLifeCycle.publish() в рабочем потоке.

    • V − тип результата задачи, он передается в метод done(). Его также можно получить вызовом метода BackgroundTaskHandler.getResult(), что приведет к ожиданию завершения задачи.

  • Метод canceled() вызывается только в случае управляемой отмены задачи, то есть при вызове cancel() у TaskHandler.

  • Если у задачи истек таймаут, или было закрыто окно, в котором она исполнялась, то задача будет завершена без уведомлений. В блоке Web Client завершение по таймауту производится с задержкой, задаваемой свойством приложения cuba.backgroundWorker.maxClientLatencySeconds .

  • Метод run() задачи должен поддерживать возможность прерывания извне. Для этого в долгих процессах желательно периодически проверять флаг TaskLifeCycle.isInterrupted(), и соответственно завершать выполнение. Кроме того, нельзя тихо проглатывать исключение InterruptedException (или вообще все исключения). Вместо этого нужно либо вообще не перехватывать его, либо выполнять корректный выход из метода.

  • Объекты BackgroundTask не имеют состояния. Если при реализации конкретного класса задачи не заводить полей для хранения промежуточных данных, то можно запускать несколько параллельно работающих процессов, используя единственный экземпляр задачи.

  • Объект BackgroundHandler можно запускать (т.е. вызывать его метод execute()) всего один раз. Если требуется частый перезапуск задачи, то используйте класс BackgroundTaskWrapper.

  • Для показа пользователю модального окна с прогрессом и кнопкой Отмена используйте классы BackgroundWorkWindow или BackgroundWorkProgressWindow с набором статических методов. Для окна можно задать режим отображения прогресса и разрешить или запретить отмену фоновой задачи.

  • Если внутри потока задачи необходимо использовать некоторые значения визуальных компонентов, то нужно реализовать их получение в методе getParams(), который выполняется в потоке UI один раз при запуске задачи. В методе run() эти параметры будут доступны через метод getParams() объекта TaskLifeCycle.

  • При возникновении исключительных ситуаций в потоке UI вызывается метод BackgroundTask.handleException(), в котором можно отобразить ошибку.

4.5.6.2. Настройка окружения

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

  • Прерывание задач по таймауту реализуется бином WatchDog. Для его периодического вызова в файлы spring.xml блоков Web Client и Desktop Client необходимо добавить следующее объявление:

    <bean id="backgroundWorkerScheduler" class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
      <property name="daemon" value="true"/>
      <property name="poolSize" value="1"/>
    </bean>
    
    <task:scheduled-tasks scheduler="backgroundWorkerScheduler">
      <task:scheduled ref="cuba_BackgroundWorker_WatchDog" method="cleanupTasks" fixed-delay="2000"/>
    </task:scheduled-tasks> 
  • В блоке Web Client опрос состояния задачи инициируется клиентским кодом, выполняющимся в веб-браузере. Периодичность опроса задается свойством приложения cuba.backgroundWorker.uiCheckInterval , по умолчанию - 2 сек.

    Кроме того, на выполнение фоновых задач в блоке Web Client влияют свойства приложения cuba.backgroundWorker.maxActiveTasksCount и cuba.backgroundWorker.maxClientLatencySeconds .

4.5.7. Создание темы приложения

Тема служит для управления визуальным представлением приложения.

4.5.7.1. Тема в веб-приложениях

Тема веб-приложения состоит из файлов SCSS и других ресурсов, в том числе файлов изображений.

4.5.7.1.1. Использование существующих тем

Платформа включает в себя две готовые темы: Halo и Havana. Приложение будет по умолчанию использовать ту из них, которая указана в свойстве приложения cuba.web.theme. Пользователь может выбрать другую доступную тему в стандартном экране Help -> Settings. Если вы не хотите, чтобы пользователи имели возможность сами выбирать тему, зарегистрируйте экран settings в файле web-screens.xml проекта с параметром changeThemeEnabled = false:

<screen id="settings" template="/com/haulmont/cuba/web/app/ui/core/settings/settings-window.xml">
  <param name="changeThemeEnabled" value="false"/>
</screen>          

При использовании существующих тем можно настроить некоторые параметры фирменного стиля (branding): пиктограммы и заголовки окна логина и главного окна, пиктограмму вебсайта favicon.ico. Для этого необходимо выполнить следующее:

  1. Создайте следующую файловую структуру внутри каталога modules/web проекта:

    themes/
      havana/
        branding/
          myapp-login.png
          myapp-menu.png
        favicon.ico

    Здесь havana - каталог с именем используемой темы, favicon.ico - пиктограмма вебсайта, myapp-login.png - изображение для окна логина, myapp-menu.png - изображение для главного окна.

  2. В CUBA Studio откройте Project properties -> Edit и нажмите кнопку Branding внизу страницы. Используя ссылки Set application logo image и Set login window logo image задайте путь к файлам пиктограмм соответственно главного окна и окна логина. Путь указывается относительно каталога темы. Остальные ссылки служат для задания заголовков окон и текста приглашения окна логина.

    Данные параметры сохраняются в главном пакете сообщений модуля gui (то есть в файле modules/gui/<root_package>/gui/messages.properties и его вариантах для разных локалей). Использование пакетов сообщений дает возможность использовать разные файлы изображений для разных локалей пользователей. Пример содержимого файла messages.properties:

    application.caption = MyApp
    application.logoImage = branding/myapp-menu.png
    
    loginWindow.caption = MyApp Login
    loginWindow.welcomeLabel = Welcome to MyApp!
    loginWindow.logoImage = branding/myapp-login.png

    Путь к favicon.ico указывать не нужно, он должен обязательно находится в корне каталога с именем темы.

В существующую тему можно также добавить файлы изображений для использования в свойствах icon действий и визуальных компонентов, например Button.

Например, чтобы добавить в тему Havana пиктограмму, достаточно в описанный выше каталог modules/web/themes/havana добавить файл изображения (желательно в некоторый подкаталог):

themes/
  havana/
    images/
      address-book.png

После этого можно использовать эту пиктограмму, указывая в свойстве icon путь к ней относительно каталога темы:

<action id="adresses"
      icon="images/address-book.png"/>

Вместо файлов изображений для пиктограмм можно использовать элементы шрифта Font Awesome. Для этого достаточно указать в свойстве icon имя нужной константы перечисления com.vaadin.server.FontAwesome с префиксом font-icon:, например:

<action id="adresses"
      icon="font-icon:BOOK"/>

В проекте можно заменить изображения, используемые в стандартных действиях и экранах платформы. Для замены пиктограммы темы Havana достаточно положить в каталог modules/web/themes/havana/icons проекта нужный файл изображения. Например, для замены пиктограммы стандартного действия create это будет файл с именем create.png (имя файла легко определить по URL соответствующего HTML-элемента img в работающем приложении):

themes/
  havana/
    icons/
      create.png

В теме Halo по умолчанию (при включенном свойстве приложения cuba.web.useFontIcons) пиктограммы стандартных действий и экранов платформы загружаются из шрифта Font Awesome. В этом случае заменить стандартную пиктограмму можно только создав свою тему на базе Halo (см. далее) и задав в файле <your_theme>-theme.properties нужное соответствие между именем пиктограммы и именем элемента шрифта, например:

cuba.web.icons.create.png = PLUS

При выключенном свойстве cuba.web.useFontIcons пиктограммы стандартных действий и экранов платформы для темы Halo загружаются так же, как и для темы Havana - из файлов изображений в подкаталоге icons. Поэтому их можно заменить описанным для Havana способом.

Тема Halo поддерживает свойство приложения cuba.web.useInverseHeader, управляющее цветом заголовка приложения. По умолчанию это свойство установлено в true, что задает темный (инверсный) заголовок. В проекте можно не изменяя темы сделать заголовок светлым, установив данное свойство в false.

4.5.7.1.2. Расширение существующей темы

Существующая в платформе тема может быть изменена в проекте приложения. Тема описывается в файлах SCSS в определенной файловой структуре, поэтому простейший способ адаптации - изменение базовых переменных SCSS, влияющих на цвет фона, размеры компонентов и отступы между ними. Для изменения параметров отдельных компонентов требуется знание CSS.

Для адаптации (расширения) темы в проекте необходимо создать специальную файловую структуру в модуле web. Это удобно сделать с помощью CUBA Studio: откройте секцию Project properties и нажмите ссылку Create theme extension. В диалоговом окне выберите тему, которую вы хотите расширить. В результате в проекте будет создана структура каталогов, аналогичная описанной в предыдущем разделе. Кроме того, скрипт сборки build.gradle будет дополнен задачей buildScssThemes, автоматически запускаемой при сборке модуля web.

Рассмотрим пример расширения темы Halo, так как она основана на теме Valo фреймворка Vaadin, и предоставляет максимальные возможности адаптации.

Файл themes/halo/halo-ext-defaults.scss предназначен для размещения в нем переменных темы. Большинство переменных Halo соответствует описанным в документации по Valo, ниже приведены основные:

$v-background-color: #fafafa;        /* цвет фона компонентов */
$v-app-background-color: #e7ebf2;    /* цвет фона приложения */
$v-panel-background-color: #fff;     /* цвет фона панелей */
$v-focus-color: #3b5998;             /* цвет выделения компонентов в фокусе */
$v-error-indicator-color: #ed473b;   /* цвет выделения обязательных незаполненных полей */

$v-line-height: 1.35;                /* высота строк */
$v-font-size: 14px;                  /* размер шрифта */
$v-font-weight: 400;                 /* начертание шрифта */
$v-unit-size: 30px;                  /* базовый размер темы, определяет высоту кнопок, полей и другие размеры компонентов */

$v-font-size--h1: 24px;              /* размер шрифта Label со стилем h1 */
$v-font-size--h2: 20px;              /* размер шрифта Label со стилем h2 */
$v-font-size--h3: 16px;              /* размер шрифта Label со стилем h3 */

/* размеры отступов margin для контейнеров */
$v-layout-margin-top: 10px;
$v-layout-margin-left: 10px;
$v-layout-margin-right: 10px;
$v-layout-margin-bottom: 10px;

/* размер отступа между компонентами в контейнере с включенной опцией spacing */
$v-layout-spacing-vertical: 10px;
$v-layout-spacing-horizontal: 10px;

/* базовые размеры для строк таблицы */
$v-table-row-height: 30px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 7px;

/* стиль фокуса для полей ввода */
$v-focus-style: inset 0px 0px 5px 1px rgba($v-focus-color, 0.5);
/* стиль, применяемый к обязательным полям ввода в фокусе */
$v-error-focus-style: inset 0px 0px 5px 1px rgba($v-error-indicator-color, 0.5);

/* анимация элементов по умолчанию включена */
$v-animations-enabled: true;
/* анимация всплывающих окон по умолчанию выключена */
$v-window-animations-enabled: false;

/* инверсный заголовок управляется свойством cuba.web.useInverseHeader */
$v-support-inverse-menu: true;

Пример содержимого файла 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;

Для изменения параметров отдельных компонентов необходимо добавить соответствующий код CSS в блок @mixin halo-ext {...} файла halo-ext.scss. Например, для того, чтобы пункты главного меню отображались жирным шрифтом, содержимое файла halo-ext.scss должно быть следующим:

@import "../halo/halo";

@mixin halo-ext {
@include halo;

.v-menubar-menuitem-caption {
  font-weight: bold;
}
}
4.5.7.1.3. Создание новой темы

В проекте можно создать одну или несколько новых тем и дать возможность пользователям выбирать среди них подходящую. Создание новой темы позволяет также переопределять переменные файла *-theme.properties, задающие некоторые параметры, требуемые на стороне сервера:

  • Размеры диалоговых окон по умолчанию.

  • Ширина полей ввода по умолчанию.

  • Размеры некоторых компонентов (Filter, FileMultiUploadField).

  • Соответствие между именами пиктограмм и именами констант перечисления com.vaadin.server.FontAwesome для использования элементов шрифта Font Awesome в стандартных действиях и экранах платформы при включенном свойстве cuba.web.useFontIcons.

Рассмотрим пример создания на основе Halo новой темы Facebook, напоминающей интерфейс сайта известной социальной сети.

  1. В CUBA Studio откройте секцию Project properties и нажмите ссылку Create theme extension. В диалоговом окне выберите halo и нажмите Create. В проекте будет создано расширение темы Halo, как описано в предыдущем разделе.

  2. Переименуйте созданный в модуле web каталог themes/halo в themes/facebook, внутри него файл halo-ext.scss переименуйте в facebook.scss, halo-ext-defaults.scss в facebook-defaults.scss.

  3. Отредактируйте файл styles.scss, заменив в нем импорты halo-ext и корневой селектор halo:

    @import "halo-defaults";
    @import "facebook-defaults";
    @import "facebook";
    
    .facebook {
    @include facebook;
    }
    
    .v-theme-version {
    display: none;
    }
  4. Отредактируйте файл facebook.scss, заменив в нем @mixin halo-ext:

    @import "../halo/halo";
    
    @mixin facebook {
    @include halo;
    }
  5. Поместите в файл facebook-defaults.scss следующие переменные:

    $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);
    $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);
  6. Создайте в подкаталоге src модуля web файл facebook-theme.properties со следующим содержимым:

    @include=halo-theme.properties

    При необходимости в этом файле можно переопределять server-side переменные темы, заданные в файле halo-theme.properties платформы.

  7. В файл web-app.properties добавьте следующие свойства:

    cuba.web.theme = facebook
    cuba.themeConfig = havana-theme.properties halo-theme.properties facebook-theme.properties
  8. Пересоберите приложение и запустите сервер. Теперь при первом входе пользователь увидит приложение в теме Facebook, и в окне Help -> Settings сможет выирать между темами Facebook, Halo, Havana.

4.5.7.2. Тема в десктоп-приложениях

В десктоп-приложениях базовой темой является тема Nimbus.

Для внесения изменения в стандартную тему нужно создать пакет res.nimbus в пакете com.sample.sales.desktop модуля desktop. В пакете res.nimbus будут храниться файлы темы.

Рисунок 25.


В папке icons хранятся файлы пиктограмм, в файле nimbus.xml − описание стиля темы.

В файле свойств для десктоп-приложения нужно установить свойство cuba.desktop.resourceLocations (задает набор директорий, в которых расположены файлы стилей):

cuba.desktop.resourceLocations = \
com/haulmont/cuba/desktop/res \
com/sample/sales/desktop/res

Примеры

  1. Добавление пиктограммы.

    Если в десктоп-приложении требуется добавить новую пиктограмму, например, для кнопки, нужно создать пакет res.nimbus.icons в пакете com.sample.sales.desktop модуля desktop и поместить в него требуемое изображение.

    Рисунок 26.


    Описываем кнопку в дескрипторе, указывая в атрибуте icon путь до пиктограммы:

    <button id="button1" caption="Attention"  icon="icons/attention.png"/>

    Ниже представлена кнопка с пиктограммой attention.png

    Рисунок 27.


  2. Переопределение значений свойств темы, установленных по умолчанию.

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

    В пакете res.nimbus нужно создать файл nimbus.xml следующего содержания:

    <theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
      <ui-defaults>
          <color property="cubaRequiredBackground" value="#f78260"/>
      </ui-defaults>
    </theme>

    Элемент ui-defaults служит для переопределения значений свойств темы платформы, установленных по умолчанию.

    В элементе ui-defaults присутствуют как свойства, содержащиеся в стандартной теме Nimbus (http://docs.oracle.com/javase/tutorial/uiswing/lookandfeel/_nimbusDefaults.html), так и свойства, созданные в платформе.

    В данном примере переопределено значение свойства платформы cubaRequiredBackground, хранящего цвет фона поля, обязательного для заполнения. Данное изменение коснется всех полей, обязательных для ввода.

  3. Создание стиля для элемента с помощью стандартных средств.

    Рассмотрим пример выделения надписи жирным цветом.

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

    <theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
      <style name="boldlabel">
          <font style="bold"/>
      </style>
    </theme>

    Элемент style может содержать другие элементы, в которых можно определять те или иные свойства: background, foreground, icon.

    В описании компонента надписи в xml-дескрипторе, к которой нужно применить созданный стиль, нужно указать атрибут stylename с именем стиля:

    <label id="label1" value="msg://labelVal" stylename="boldlabel"/>

    Таким образом, данный стиль будет применен только к тем надписям, для которых определен атрибут stylename со значением boldlabel.

  4. Создание пользовательского стиля.

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

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

    В первую очередь создадим класс-декоратор UnderlinedLabelDecorator:

    public class UnderlinedLabelDecorator implements ComponentDecorator {
    
      @Override
      @SuppressWarnings("unchecked")
      public void decorate(Object component, Set<String> state) {
          DesktopLabel item = (DesktopLabel) component;
          JLabel jlabel = item.getComponent();
    
          Font originalFont = jlabel.getFont();
          Map attributes = originalFont.getAttributes();
          attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
          jlabel.setFont(originalFont.deriveFont(attributes));
      }
    }

    Определим пользовательский стиль в файле nimbus.xml:

    <theme xmlns="http://schemas.haulmont.com/cuba/desktop-theme.xsd">
      <style name="label-underlined" component="com.haulmont.cuba.desktop.gui.components.DesktopLabel">
          <custom class="com.sample.sales.desktop.gui.decorators.UnderlinedLabelDecorator"/>
      </style>
    </theme>

    В атрибуте component элемента style содержится название компонента, к которому может быть применен стиль с названием label-underlined.

    В элементе custom указывается путь до класса-декоратора, определенного ранее.

    При описании элемента надписи, к которой нужно применить пользовательский стиль, нужно в атрибуте stylename указать название стиля:

    <label id="label1" stylename="label-underlined" value="Label"/>

    Рисунок 28. Компонент надписи с пользовательским стилем

    Компонент надписи с пользовательским стилем

4.5.8. Специфика Web Client

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

Рисунок 29. Классы инфраструктуры Web Client

Классы инфраструктуры Web Client


  • App - центральный класс инфраструктуры приложения. Позволяет получить ссылки на Connection, AppWindow и другие объекты инфраструктуры. Экземпляр App существует в единственном экземпляре для данной HTTP-сессии пользователя.

    В конкретном приложении, как правило, создается собственный класс App, унаследованный от DefaultApp и, соответственно, от базового абстрактного App платформы. Это позволяет переопределить методы createAppWindow() и createLoginWindow() для создания собственных реализаций главного окна и окна логина.

    Класс App приложения должен быть зарегистрирован в параметре application сервлета app_servlet в файле web.xml модуля web.

  • Connection - интерфейс, обеспечивающий функциональность подключения к среднему слою и хранящий пользовательскую сессию UserSession. Стандартной реализацией этого интерфейса является класс DefaultConnection.

  • AppUI - класс платформы, унаследованный от класса com.vaadin.ui.UI. Экземпляр данного класса соответствует одной открытой вкладке веб браузера. Содержит ссылку на объект UIView - это может быть либо LoginWindow, либо AppWindow.

    Класс AppUI приложения должен быть зарегистрирован в параметре UI сервлета app_servlet в файле web/WEB-INF/web.xml модуля web. Как правило используется стандартный класс платформы.

  • LoginWindow - окно, отображаемое до логина пользователя. В конкретном приложении можно создать наследника LoginWindow и переопределить метод createLoginWindow() класса App для его использования.

  • AppWindow - главное окно приложения, отображаемое после логина пользователя. В конкретном приложении можно создать наследника AppWindow и переопределить метод createAppWindow() класса App для его использования.

    Метод onHistoryBackPerformed() позволяет обработать нажатия на кнопку Back браузера. Этот метод вызывается вместо стандартного поведения браузера если свойство приложения cuba.web.allowHandleBrowserHistoryBack установлено в true.

    Без создания собственного наследника AppWindow можно управлять некоторыми параметрами главного окна с помощью следующих свойств приложения:

    • cuba.web.useLightHeader - включает формирование компактной вехней части окна - лого, строка меню, имя пользователя и кнопка логаута в одну строку. В выключенном состоянии методом AppWindow.createTitleLayout() формируется дополнительная область сверху.

    • cuba.web.foldersPaneEnabled - включает формирование панели папок методом AppWindow.createFoldersPane().

    • cuba.web.appWindowMode - задает начальный режим главного окна: с вкладками или одноэкранный (TABBED или SINGLE). Пользователь впоследствии может задать желаемый режим через экран Help > Settings.

    • cuba.web.maxTabCount - в режиме представления главного окна с вкладками задает максимальное количество вкладок, которое может открыть пользователь. По умолчанию 7.

  • WindowManager - центральный класс, реализующий логику работы экранов системы. Ему делегируются вызовы openWindow(), openEditor(), showMessageDialog() и другие методы интерфейса IFrame, реализуемого контроллерами экранов. Класс WindowManager расположен в общем модуле gui платформы и является абстрактным. В модуле web имеется конкретный класс WebWindowManager, реализующий специфику веб клиента.

    Как правило, WindowManager не используется в прикладном коде напрямую.

  • ExceptionHandlers - содержит коллекцию обработчиков исключений клиентского уровня.

4.5.8.1. Работа с компонентами Vaadin

Для работы непосредственно с компонентами Vaadin, реализующими интерфейсы библиотеки визуальных компонентов в блоке Web Client, необходимо воспользоваться классом WebComponentsHelper. Он имеет два статических метода для получения ссылок на компоненты Vaadin:

  • unwrap - получить Vaadin-компонент для данного CUBA-компонента.

  • getComposition - получить Vaadin-компонент, который является наиболее внешним контейнером в реализации данного CUBA-компонента. Для простых компонентов, например Button, этот метод возвращает тот же объект, что и unwrap() - com.vaadin.ui.Button. Для сложных компонентов, например Table, unwrap() вернет соответсвующий объект com.vaadin.ui.Table, а getComposition() - объект com.vaadin.ui.VerticalLayout, который содержит таблицу вместе с описанными вместе с ней ButtonsPanel и RowsCount.

Следует иметь в виду, что если экран расположен в модуле gui проекта, то в его контроллере можно работать только с обобщенными интерфейсами CUBA-компонентов. Чтобы использовать WebComponentsHelper.unwrap() нужно либо расположить весь экран в модуле web, либо воспользоваться механизмом компаньонов контроллеров.

4.5.8.2. Компоновка главного окна приложения

Механизм предоставляет возможность задавать компоновку главного экрана веб-приложения с использованием технологии универсального пользовательского интерфейса CUBA - XML-дескриптора и Java-контроллера с применением визуальных компонентов и источников данных.

Главное окно - особый экран системы, имеющий идентификатор mainWindow. Контроллер главного экрана должен быть наследником классаAbstractMainWindow.

Помимо стандартных компонентов GUI в главном экране приложения можно использовать дополнительные компоненты:

  • AppMenu - главное меню.

  • FoldersPane - панель папок поиска и папок приложения.

  • AppWorkArea - рабочая область, обязательный компонент для работы с экранами в режимах THIS_TAB, NEW_TAB и NEW_WINDOW.

  • UserIndicator - поле, отображающее имя текущего пользователя, а при наличии замещаемых пользователей позволяет переключаться между ними.

  • NewWindowButton - кнопка открытия нового окна приложения.

  • LogoutButton - кнопка выхода из приложения.

  • TimeZoneIndicator - надпись, которая отображает часовой пояс пользователя.

  • FtsField - поле полнотекстового поиска.

Для работы с дополнительными компонентами в XML-дескриптор экрана нужно добавить элемент xmlns:main:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
        class="com.company.sample.gui.MainWindow">
    <layout>
    </layout>
</window>

Специальный компонент AppWorkArea представляет собой рабочую область, в которой открываются экраны приложения. Если свойство приложения cuba.web.appWindowMode имеет значение TABBED (по умолчанию), то на месте рабочей области будет расположен компонент TabSheet с экранами приложения. В противном случае рабочая область будет содержать единственный открытый экран. Когда не открыт ни один экран, рабочая область содержит компоненты, определенные во вложенном элементе initialLayout:

<main:workArea id="workArea" width="100%" height="100%">
    <main:initialLayout spacing="true" margin="true">
        <!-- content shown when there are no open screens -->
    </main:initialLayout>
</main:workArea>

При открытии экранов компоновка начального экрана (initialLayout) удаляется из AppWorkArea, при закрытии всех экранов - добавляется обратно. Для реакции на события смены рабочей области на стартовый экран и на отображение экранов приложения можно добавить обработчик AppWorkArea.StateChangeListener. Например, в таком слушателе можно разместить код обновления данных стартового экрана.

В платформе существует стандартная реализация главного окна приложения. Ее XML-дескриптор - /com/haulmont/cuba/web/app/mainwindow/mainwindow.xml, соответствующий контроллер - AppMainWindow. Стандартная реализация главного окна может быть расширена в проекте, так же как обычный экран системы. Пример расширяющего экрана:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
        extends="com/haulmont/cuba/web/app/mainwindow/mainwindow.xml"
        class="com.haulmont.cuba.web.app.mainwindow.AppMainWindow">
    <layout>
        <vbox ext:index="0">
            <label value="This is my main window!" stylename="h2"/>
        </vbox>
    </layout>
</window>

Этот экран должен быть зарегистрирован в screens.xml с идентификатором mainWindow.

Реализация главного окна может быть полностью заменена. Например:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
        class="com.company.sample.gui.MainWindow">
    <layout expand="middlePanel">
        <hbox margin="true"
              stylename="gray"
              width="100%">
            <label align="MIDDLE_CENTER"
                   value="Header"/>
        </hbox>
        <main:menu width="100%"/>
        <split id="middlePanel"
               orientation="horizontal"
               pos="80"
               width="100%">
            <main:workArea id="workArea"
                           height="100%"
                           width="100%">
                <main:initialLayout stylename="red">
                    <label align="MIDDLE_CENTER"
                           value="Work Area (Initial Layout)"/>
                </main:initialLayout>
            </main:workArea>
            <main:foldersPane height="100%"
                              stylename="blue"
                              width="100%"/>
        </split>
        <hbox margin="true"
              stylename="gray"
              width="100%">
            <label align="MIDDLE_CENTER"
                   value="Footer"/>
        </hbox>
    </layout>
</window>

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

Оно же с открытым экраном:

Свойство приложения cuba.web.showBreadCrumbs позволяет скрыть панель навигации (breadcrumbs) над открытым экраном.

4.5.9. Специфика Desktop Client

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

Рисунок 30. Классы инфраструктуры Desktop Client

Классы инфраструктуры Desktop Client


  • App - центральный класс инфраструктуры десктоп приложения. Содержит ссылки на Connection и главный TopLevelFrame, а также методы инициализации и получения параметров приложения (см. ниже).

    В конкретном приложении необходимо создать собственный класс-наследник App и переопределить в нем следующие методы:

    • getDefaultAppPropertiesConfig - должен возвращать строку, в которой через пробел перечислены файлы свойств приложения, например:

      @Override
      protected String getDefaultAppPropertiesConfig() {
        return "/cuba-desktop-app.properties /desktop-app.properties";
      }
    • getDefaultHomeDir - должен вовращать путь к каталогу, в котором приложение будет хранить временные и рабочие файлы, например:

      @Override
      protected String getDefaultHomeDir() {
        return System.getProperty("user.home") + "/.mycompany/sales";
      }
    • getDefaultLog4jConfig - должен возвращать имя файла настройки Log4J, если таковой определен в проекте. Например:

      @Override
      protected String getDefaultLog4jConfig() {
        return "sales-log4j.xml";
      }

    Кроме того, в собственном классе-наследнике App необходимо определить метод main() следующим образом:

    public static void main(final String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
          public void run() {
              app = new App();
              app.init(args);
              app.show();
              app.showLoginDialog();
          }
      });
    }
  • Connection - класс, обеспечивающий функциональность подключения к среднему слою и хранящий пользовательскую сессию UserSession.

  • LoginDialog - диалог логина пользователя. В конкретном приложении можно создать наследника LoginDialog и переопределить метод createLoginDialog() класса App для его использования.

  • TopLevelFrame - наследник JFrame, являющийся окном самого верхнего уровня. В приложении существует как минимум один экземпляр данного класса, создаваемый при старте приложения и содержащий главное меню. Этот экземпляр возвращается методом getMainFrame() класса App.

    При отделении пользователем вкладок главного окна или компонента TabSheet (см. атрибут detachable) создаются дополнительные экземпляры TopLevelFrame, не содержащие главного меню.

  • WindowManager - центральный класс, реализующий логику работы экранов системы. Ему делегируются вызовы openWindow(), openEditor(), showMessageDialog() и другие методы интерфейса IFrame, реализуемого контроллерами экранов. Класс WindowManager расположен в общем модуле gui платформы и является абстрактным. В модуле desktop имеется конкретный класс DesktopWindowManager, реализующий специфику десктоп клиента.

    Как правило, WindowManager не используется в прикладном коде напрямую.

  • ExceptionHandlers - содержит коллекцию обработчиков исключений клиентского уровня.

4.5.9.1. Работа с компонентами Swing

Для работы непосредственно с компонентами Swing, реализующими интерфейсы библиотеки визуальных компонентов в блоке Desktop Client, необходимо воспользоваться классом DesktopComponentsHelper. Он имеет два статических метода для получения ссылок на компоненты Swing:

  • unwrap - получить Swing-компонент для данного CUBA-компонента.

  • getComposition - получить Swing-компонент, который является наиболее внешним контейнером в реализации данного CUBA-компонента. Для простых компонентов, например Button, этот метод возвращает тот же объект, что и unwrap() - javax.swing.JButton. Для сложных компонентов, например Table, unwrap() вернет соответсвующий объект org.jdesktop.swingx.JXTable, а getComposition() - объект javax.swing.JPanel, который содержит таблицу вместе с описанными вместе с ней ButtonsPanel и RowsCount.

Следует иметь в виду, что если экран расположен в модуле gui проекта, то в его контроллере можно работать только с обобщенными интерфейсами CUBA-компонентов. Чтобы использовать DesktopComponentsHelper.unwrap() нужно либо расположить весь экран в модуле desktop, либо воспользоваться механизмом компаньонов контроллеров.

4.5.10. Создание собственных компонентов

В данном разделе рассматривается процесс создания и использования собственных визуальных компонентов приложения. Сначала мы возьмем сторонний компонент, доступный в виде дополнения (add-on) Vaadin, подключим его в проект и будем использовать в экране непосредственно. Затем выполним более тесную интеграцию - создадим для компонента новый GUI-интерфейс и загрузчик из XML, что позволит использовать его аналогично всем остальным компонентам платформы.

4.5.10.1. Использование сторонних компонентов Vaadin

В веб клиенте приложения можно использовать сторонние компоненты Vaadin, распространяемые в виде дополнений (add-ons). На данный момент в репозитории https://vaadin.com/directory находится около 200 визуальных компонентов, совместимых с CUBA. Основное условие совместимости - компонент должен поддерживать Vaadin версии 7 и выше.

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

  1. Добавить в проект модуль web-toolkit, выполняющий интеграцию с клиентской (браузерной) частью Vaadin-компонентов. Проще всего это сделать в CUBA Studio, выполнив команду Create web toolkit module секции Project properties навигатора.

  2. В build.gradle проекта добавить зависимость модуля web от нужного add-on, например:

    configure(webModule) {
      ...
      dependencies {
          ...
          compile("org.vaadin.addons:some-addon:1.2.3")
      }
  3. В созданный на шаге 1 файл AppWidgetSet.gwt.xml подключить набор виджетов add-on:

    <module>
      ...
      <inherits name="org.vaadin.someaddon.widgetset.SomeAddonWidgetset" />
  4. В экране модуля web (либо в соответствующем компаньоне) получить ссылку на контейнер Vaadin с помощью класса WebComponentsHelper, создать экземпляр нового компонента, и добавить его в контейнер.

  5. Для изменения внешнего вида подключенного компонента можно создать в проекте расширение темы и внести в файл <theme>-ext.scss нужные изменения. Файлы темы проще всего создать в Studio командой Create theme extension секции Project properties навигатора.

В Раздел 5.8.6.1, «Пример использования стороннего компонента Vaadin » рассмотрен процесс подключения и использования Vaadin-дополнения Stepper, содержащего визуальный компонент для пошагового изменения значения.

4.5.10.2. Интеграция компонентов в Generic UI

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

  1. Создать интерфейс компонента. Обычно интерфейсы располагаются в модуле gui, чтобы быть доступными обоим типам клиентов - веб и десктоп. Если же вы уверены, что компонент будет реализован только для одного типа клиента, интерфейс можно расположить в соответствующем модуле - web или desktop. Далее предполагается что компонент реализован только для веб клиента.

    Интерфейс компонента должен быть унаследован от com.haulmont.cuba.gui.components.Component или какого-либо его наследника, например DatasourceComponent или Field:

    package com.company.myproject.gui.components;
    
    import com.haulmont.cuba.gui.components.Component;
    
    public interface MyComponent extends Component {
    
      String NAME = "myComponent";
    
      int getSomeParameter();
      void setSomeParameter(int value);
    }

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

  2. Создать класс имплементации компонента в модуле web.

    Класс компонента рекомендуется унаследовать от com.haulmont.cuba.web.gui.components.WebAbstractComponent или какого-либо его наследника, например WebAbstractField. В конструкторе класса создается экземпляр "нативного" компонента, и ему делегируются вызовы методов GUI-интерфейса:

    package com.company.myproject.web.components;
    
    import com.company.myproject.gui.components.MyComponent;
    import com.haulmont.cuba.web.gui.components.WebAbstractComponent;
    
    public class WebMyComponent
          extends WebAbstractComponent<org.vaadin.someaddon.SomeComponent>
          implements MyComponent {
    
      public WebMyComponent() {
          component = new org.vaadin.someaddon.SomeComponent();
      }
    
      @Override
      public int getSomeParameter() {
          return component.getSomeParameter();
      }
    
      @Override
      public void setSomeParameter(boolean value) {
          component.setSomeParameter(value);
      }
    }
  3. Создать класс, имплементирующий интерфейс ComponentPalette, и из его метода getComponents() вернуть мэп имен своих компонентов на их классы реализации:

    package com.company.myproject.web;
    
    import com.company.myproject.gui.components.MyComponent;
    import com.company.myproject.web.components.WebMyComponent;
    import com.haulmont.cuba.gui.ComponentPalette;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.xml.layout.ComponentLoader;
    import java.util.HashMap;
    import java.util.Map;
    
    public class AppComponentPalette implements ComponentPalette {
    
      @Override
      public Map<String, Class<? extends Component>> getComponents() {
          Map<String, Class<? extends Component>> components = new HashMap<>();
          components.put(MyComponent.NAME, WebMyComponent.class);
          return components;
      }
    
      @Override
      public Map<String, Class<? extends ComponentLoader>> getLoaders() {
          return Collections.emptyMap();
      }
    }

    Экземпляр палитры компонентов необходимо зарегистрировать в приложении. Это можно сделать в блоке инициализации класса App:

    package com.company.myproject.web;
    
    import com.haulmont.cuba.web.DefaultApp;
    import com.haulmont.cuba.web.gui.WebUIPaletteManager;
    
    public class App extends DefaultApp {
    
      static {
          WebUIPaletteManager.registerPalettes(new AppComponentPalette());
      }
    }
  4. На данном этапе новый GUI-компонент доступен для получения через ComponentsFactory:

    @Inject
    private BoxLayout box;
    @Inject
    private ComponentsFactory componentsFactory;
    
    @Override
    public void init(Map<String, Object> params) {
      MyComponent myComponent = componentsFactory.createComponent(MyComponent.NAME);
      box.addComponent(myComponent);
      ...
    }
  5. Для поддержки объявления компонента в XML-дескрипторах экранов необходимо создать класс-загрузчик компонента, реализующий интерфейс com.haulmont.cuba.gui.xml.layout.ComponentLoader. Класс-загрузчик рекомендуется унаследовать от класса com.haulmont.cuba.gui.xml.layout.loaders.ComponentLoader или какого-либо его наследника. Загрузчик оперирует только с GUI-интерфейсом компонента, поэтому он является общим для всех типов клиентов и его можно разместить в модуле gui. В загрузчике достаточно вызвать унаследованный метод loadComponent(), который создает экземпляр компонента и устанавливает ему из XML общие свойства, такие как идентификатор, размеры и пр. После этого можно проинициализировать специфические свойства компонента:

    package com.company.myproject.gui.loaders;
    
    import com.company.myproject.gui.components.MyComponent;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.xml.layout.*;
    import org.dom4j.Element;
    
    public class MyComponentLoader extends ComponentLoader {
    
      public MyComponentLoader(Context context, LayoutLoaderConfig config, ComponentsFactory factory) {
          super(context, config, factory);
      }
    
      @Override
      public Component loadComponent(ComponentsFactory factory, Element element, Component parent) {
          MyComponent component = (MyComponent) super.loadComponent(factory, element, parent);
    
          String someParameter = element.attributeValue("someParameter");
          if (someParameter != null) {
              component.setSomeParameter(Integer.valueOf(someParameter));
          }
          return component;
      }
    }

    Для того, чтобы система нашла загрузчик, необходимо зарегистрировать его с помощью метода getLoaders() созданной ранее палитры компонентов:

    public class AppComponentPalette implements ComponentPalette {
      ...
    
      @Override
      public Map<String, Class<? extends ComponentLoader>> getLoaders() {
          Map<String, Class<? extends ComponentLoader>> loaders = new HashMap<>();
          loaders.put(MyComponent.NAME, MyComponentLoader.class);
          return loaders;
      }
    }
  6. Теперь компонент можно использовать и в XML-дескрипторах экранов проекта:

    <layout>
      <myComponent id="someId" width="100%" someParameter="10"/>
    </layout>

    Для того, чтобы IDE подсказывала имя компонента и его атрибуты, можно определить собственную XSD и включать ее в экранах:

    <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
          xmlns:app="http://schemas.company.com/app/0.1/app-components.xsd"
          ...>
    
      <layout>
          <app:myComponent id="someId" width="100%" someParameter="10"/>
      </layout>

В Раздел 5.8.6.2, «Пример интеграции компонента Vaadin в Generic UI» рассмотрен процесс интеграции в универсальный UI компонента IntStepper, предназначенного для пошагового изменения целого значения.

4.5.11. Горячие клавиши

В данном разделе приведена информация обо всех горячих клавишах (shortcuts), которые используются по умолчанию в универсальном пользовательском интерфейсе приложения. Все перечисленные ниже свойства приложения принадлежат интерфейсу ClientConfig и используются в блоках Web Client и Desktop Client.

  • Главное окно приложения.

    • CTRL-SHIFT-PAGE_DOWN - переход на следующую вкладку. Настраивается свойством приложения cuba.gui.nextTabShortcut.

    • CTRL-SHIFT-PAGE_UP - переход на предыдущую вкладку. Настраивается свойством приложения cuba.gui.previousTabShortcut.

  • Экраны.

    • ESCAPE - закрыть текущий экран. Настраивается свойством приложения cuba.gui.closeShortcut.

    • CTRL-ENTER - закрыть текущий экран редактирования с сохранением изменений. Настраивается свойством приложения cuba.gui.commitShortcut.

  • Стандартные действия компонента-списка (Table, GroupTable, TreeTable, Tree). Кроме указанных свойств приложения горячая клавиша для конкретного экземпляра действия может быть установлена его методом setShortcut().

    • CTRL-INSERT - вызов действия CreateAction. Настраивается свойством приложения cuba.gui.tableInsertShortcut.

    • CTRL-ALT-INSERT - вызов действия AddAction. Настраивается свойством приложения cuba.gui.tableAddShortcut.

    • ENTER - вызов действия EditAction. Настраивается свойством приложения cuba.gui.tableEditShortcut.

    • CTRL-DELETE - вызов действий RemoveAction и ExcludeAction. Настраивается свойством приложения cuba.gui.tableRemoveShortcut.

  • Выпадающие списки (LookupField, LookupPickerField).

    • SHIFT-DELETE – очистить значение.

  • Стандартные действия поля выбора (PickerField, LookupPickerField, SearchPickerField). Кроме указанных свойств приложения горячая клавиша для конкретного экземпляра действия может быть установлена его методом setShortcut().

    • CTRL-ALT-L - вызов действия LookupAction. Настраивается свойством приложения cuba.gui.pickerShortcut.lookup.

    • CTRL-ALT-O - вызов действия OpenAction. Настраивается свойством приложения cuba.gui.pickerShortcut.open.

    • CTRL-ALT-C - вызов действия ClearAction. Настраивается свойством приложения cuba.gui.pickerShortcut.clear.

    В полях выбора кроме вышеперечисленных горячих клавиш поддерживается вызов действий сочетанием CTRL-ALT-1, CTRL-ALT-2 и так далее по количеству действий. То есть при нажатии сочетания клавиш CTRL-ALT-1 произойдет вызов действия, которое описано первым в списке действий, при нажатии сочетания клавиш CTRL-ALT-2 − вызов второго действия и так далее. Сочетание CTRL-ALT можно заменить другим, указав его в свойстве приложения cuba.gui.pickerShortcut.modifiers.

  • Компонент Filter.

    • SHIFT-BACKSPACE – открыть список выбора фильтров. Настраивается свойством приложения cuba.gui.filterSelectShortcut.

    • SHIFT-ENTER - применить выбранный фильтр. Настраивается свойством приложения cuba.gui.filterApplyShortcut.

4.6. Компоненты портала

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

  • предоставлять альтернативный веб-интерфейс, как правило, предназначенный для пользователей за пределами организации;

  • предоставлять интерфейс для интеграции с мобильными приложениями и со сторонними системами.

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

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

4.6.1. Базовая функциональность

Рассмотрим основные компоненты, предоставляемые платформой для построения портала.

  • PortalAppContextLoader - загрузчик AppContext , должен быть зарегистрирован в элементе listener файла web.xml.

  • PortalDispatcherServlet - центральный сервлет, распределяющий запросы по контроллерам Spring MVC, как для веб-интерфейса, так и для REST API. Набор файлов конфигурации контекста Spring определяется свойством приложения cuba.dispatcherSpringContextConfig . Данный сервлет должен быть зарегистрирован в web.xml и отображен на корневой URL веб-приложения.

  • App - объект, содержащий информацию о текущем HTTP запросе и ссылку на объект Connection. Экземпляр App может быть получен в прикладном коде вызовом статического метода App.getInstance().

  • Connection - позволяет выполнять логин и логаут пользователя на Middleware.

  • PortalSession - специфичесий для портала объект пользовательской сессии. Возвращается интерфейсом инфраструктуры UserSessionSource , а также статическим методом PortalSessionProvider.getUserSession().

    Имеет дополнительный метод isAuthenticated(), возвращающий true, если данная сессия принадлежит неанонимному, т.е. явно зарегистрировавшемуся с логином и паролем, пользователю.

    При первом обращении некоторого пользователя к порталу SecurityContextHandlerInterceptor создает для него (или привязывает уже имеющуюся) анонимную сессию, регистрируясь на Middleware с именем пользователя, указанным в свойстве приложения cuba.portal.anonymousUserLogin . Регистрация производится методом loginTrusted() , поэтому в блоке портала необходимо установить также свойство cuba.trustedClientPassword . Таким образом, любой анонимный пользователь портала может работать с сервисами Middleware с правами пользователя cuba.portal.anonymousUserLogin.

    Если портал содержит страницу регистрации пользователя с именем и паролем, то после выполнения Connection.login() при обработке запросов SecurityContextHandlerInterceptor устанавливает в потоке выполнения пользовательскую сессию явно зарегистрированного пользователя, и работа с Middleware происходит от его имени.

  • PortalLogoutHandler - обрабатывает навигацию на страницу логаута. Должен быть зарегистрирован в файле portal-security-spring.xml проекта.

Пример портала, содержащего страницу регистрации пользователей, включен в шаблон проекта, рассмотренный в ???

4.6.2. REST API

Универсальный REST API платформы позволяет выполнять загрузку и сохранение любых сущностей модели данных приложения посредством отправки простых HTTP запросов. Это открывает возможность легкой интеграции со сторонними приложениями самого широкого спектра − от JavaScript кода, выполняющегося в браузере, до произвольных систем, работающих на Java, .NET, PHP или любой другой платформе.

Основные возможности API:

  • загрузка экземпляров сущностей из базы данных по идентификатору или по JPQL запросу с параметрами

  • сохранение новых и измененных экземпляров, удаление экземпляров

  • получение описания модели данных в формате HTML

  • представление данных в форматах JSON и XML на выбор

  • аутентификация пользователя

  • вызов сервисов среднего слоя.

Все функции работают с данными в кодировке UTF-8.

4.6.2.1. Включение в проект

REST API реализован в модуле portal базового проекта cuba, поэтому для его использования необходимо создать модуль portal в проекте приложения. Простейший способ сделать это - использовать команду Create portal module панели Project properties навигатора CUBA Studio.

Основные элементы настройки:

  • Добавить контроллеры REST API в контекст Spring, определяемый файлом portal-dispather-spring.xml :

    <context:component-scan base-package="com.haulmont.cuba.portal.restapi"/>
  • Установить режим доступа в portal-security-spring.xml:

    <intercept-url pattern="/api/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>

4.6.2.2. Описание функций

При стандартных настройках модуля portal все запросы к REST API должны иметь URL, начинающийся с {host:port}/app-portal/api.

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

4.6.2.2.1. Логин

Логин можно выполнить либо GET, либо POST запросом.

GET запрос

В случае GET запроса сформируйте URL {host:port}/app-portal/api/login с параметрами:

  • u − логин пользователя

  • p − пароль пользователя

  • l − локаль пользователя (опционально)

Например:

http://localhost:8080/app-portal/api/login?u=admin&p=admin&l=ru
POST запрос

Для выполнения логина через POST необходимо выполнить запрос по адресу {host:port}/app-portal/api/login, при этом в теле запроса передается JSON (заголовок Content-Type имеет значение application/json) или форма (заголовок Content-Type имеет значение application/x-www-form-urlencoded)

Пример формата JSON:

{
  "username" : "admin",
  "password" : "admin",
  "locale" : "en"
}

Пример формы:

username: admin
password: admin
locale: en

В ответ сервис вернет userSessionId в теле ответа и статус 200, либо статус 401, если аутентификация не удалась.

Чтобы иметь возможность входить через REST API, пользователь должен иметь специфическое разрешение cuba.restApi.enabled. Заметьте, что пользователь будет иметь разрешение если ни одна роль явно не отбирает его.

4.6.2.2.2. Логаут

Логаут также можно выполнить либо GET, либо POST запросом.

GET запрос

В случае GET запроса сформируйте URL {host:port}/app-portal/api/logout с параметром session - идентификатором текущей сессии, полученным вызовом login.

Например:

http://localhost:8080/app-portal/api/logout?session=64f7d59d-2cf5-acfb-f4d3-f55b7882da72
POST запрос

Для выполнения логина через POST необходимо выполнить запрос по адресу {host:port}/app-portal/api/logout, при этом в теле запроса передается JSON (заголовок Content-Type имеет значение application/json) или форма (заголовок Content-Type имеет значение application/x-www-form-urlencoded)

Пример формата JSON:

{
  "session" : "64f7d59d-2cf5-acfb-f4d3-f55b7882da72"
}

Пример формы:

session: 64f7d59d-2cf5-acfb-f4d3-f55b7882da72

В ответ сервис вернет статус 200.

4.6.2.2.3. Загрузка экземпляра персистентного объекта из базы данных по идентификатору

Для получения объекта необходимо выполнить GET запрос {host:port}/app-portal/api/find.<format>?e=<entityRef>&s=<sessionId> с параметрами:

  • e − описание требуемого объекта в формате <entity-id> или <entity-id-view>(см. класс EntityLoadInfo), например, sales$Order-43c61345-d23c-48fe-ab26-567504072f05-_local. То есть формат позволяет указать требуемое представление загруженного объекта.

  • s − идентификатор текущей сессии.

Элемент запроса format задает формат получения результата. Принимает два значения: xml или json.

Пример запроса, возвращающего результат в формате xml:

http://localhost:8080/app-portal/api/find.xml?e=sales$Order-60885987-1b61-4247-94c7-dff348347f93-orderWithCustomer&s=c38f6bf4-fae7-4ee6-a412-9d93ff243f23

Пример запроса, возвращающего результат в формате json

http://localhost:8080/app-portal/api/find.json?e=sales$Order-60885987-1b61-4247-94c7-dff348347f93-orderWithCustomer&s=c38f6bf4-fae7-4ee6-a412-9d93ff243f23
4.6.2.2.4. Выполнение JPQL запроса для выборки данных

Для выполнения запроса необходимо выполнить GET запрос {host:port}/app-portal/api/query.<format>?e=<entity>&s=<sessionId>&q=<encoded query string>&param1=<value 1>$param1_type=<type 1>&paramN=<value N>&paramN_type=<type N>&view=<viewName>&firstResult=<firstResult>&maxResults=<maxResults> с параметрами:

  • e − имя сущности

  • q − строка запроса к данным на JPQL. Запрос может содержать параметры. Их значения указываются как значения одноименных параметров HTTP запроса.

  • s − идентификатор текущей сессии

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

  • max − опционально, максимальное количество строк возвращаемых данных (аналогично JPA setMaxResults)

  • first − опционально, номер первой строки возвращаемых данных (аналогично JPA setFirstResult)

format задает формат получения результата. Принимает два значения: xml или json.

Например:

http://localhost:8080/app-portal/api/query.json?e=sales$Customer&q=select+c+from+sales$Customer+c&s=748e5d3f-1eaf-4b38-bf9d-8d838587367d&view=_local
http://localhost:8080/app-portal/api/query.json?e=sales$Customer&q=select+c+from+sales$Customer+c+where+c.name=:name&s=748e5d3f-1eaf-4b38-bf9d-8d838587367d&name=Петров

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

http://localhost:8080/app-portal/api/query.json?e=sales$Customer&q=select+c+from+sales$Customer+c+where+c.name=:name&s=748e5d3f-1eaf-4b38-bf9d-8d838587367d&name=Петров&name_type=string

Указание типа параметра не является обязательным, но позволяет избежать ошибок парсинга, если система не сможет определить тип.

В общем случае тип стоит указывать лишь для строковых параметров, которые по какой-либо причине имеют формат более узких типов (дат, чисел, uuid), но должны интерпретироваться именно как строки.

Список доступных типов можно увидеть в описании мета-модели (пункт меню Помощь −> Модель данных) или получив HTML-описание модели.

4.6.2.2.5. Коммит новых и измененных экземпляров, удаление

Функция коммита позволяет выполнять операции над переданными ей объектами и возвращает их новое состояния. Формат результата определяется тем, какой формат (JSON или XML) был использован для запроса (заголовок Content-Type).

Формат JSON

В качестве заголовка Content-Type следует использовать значение application/json.

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

{
  "commitInstances": [{
      "id": "NEW-sales$Customer",
      "name": "Saltikov",
      "email": "saltikov@mail.com"
      }
  ]
}

Создание сущности покупателя с указанным идентификатором:

{
  "commitInstances": [{
      "id": "NEW-sales$Customer-b32a6412-d4d9-11e2-a20b-87b22b1460c7",
      "name": "Titov",
      "email": "titov@mail.com"
      }
  ]
}

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

{
  "commitInstances": [{
      "id": "NEW-sales$Order",
      "amount": 15,
      "customer":
          {"id": "NEW-sales$Customer-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff"
          }
      },{
          "id": "sales$Customer-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff",
          "name": "Dudkin",
          "email": "dudkin@mail.com"
         }
  ]
}

Изменение одновременно двух сущностей покупателей:

{
  "commitInstances": [{
        "id": "sales$Customer-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff",
        "email": "dudkin@mail.ru"
      },
      {
        "id": "sales$Customer-32261b09-b7f7-4b8c-88cc-6dee6fa8e6ab",
        "email": "saltikov@mail.ru"
      }
  ]
}

Удаление сущности покупателя с поддержкой мягкого удаления:

{
  "removeInstances": [{
      "id": "sales$Customer-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff"
      }
  ],
  "softDeletion": "true"
}
  • Массив commitInstances содержит создаваемые или изменяемые сущности.

    • При создании сущности в качестве значения поля id указывается значение NEW-<entityName> или NEW-<entityName>-<entityId>.

    • При изменении сущности в качестве значения поля id указывается значение <entityName>-<entityId>.

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

      Если при изменении сущности требуется установить какой-либо атрибут в null, то в идентификаторе необходимо указать также представление, включающее этот атрибут. Например:

      {
        "commitInstances": [{
            "id": "sales$Customer-b32a6412-d4d9-11e2-a20b-87b22b1460c7-customer-edit",
            "name": "John Doe",
            "channel": null
            }
        ]
      }

      Здесь представление customer-edit должно содержать атрибут channel, в противном случае его значение не изменится. Для установки в null локального атрибута можно указать всегда доступное представление _local.

  • Массив removeInstances содержит удаляемые объекты. При удалении объекта обязательно указывать значение поля id. Перед удалением будет выполнен merge() переданных объектов, что позволяет, например, проверить, не изменилась ли версия удаляемого объекта.

  • Поле softDeletion управляет режимом мягкого удаления.

Функция вызывается посредством POST обращения к {host:port}/app-portal/api/commit?s=<sessionId>. JSON передается в теле запроса. Функция возвращает массив объектов JSON. Например, при изменении поля email у сущности покупателя будет возвращен следующий массив объектов JSON:

[
   {"id":"sales$Customer-32261b09-b7f7-4b8c-88cc-6dee6fa8e6ab",
       "createTs":"2013-06-14T14:07:15.040",
       "createdBy":"admin",
       "deleteTs":null,
       "deletedBy":null,
       "email":"saltikovvvv@mail.ru",
       "name":"Saltikov",
       "updateTs":"2013-06-14T15:07:03.463",
       "updatedBy":"admin",
       "version":"3"
   }
]                   
Формат XML

В качестве заголовка Content-Type следует использовать значение text/xml.

Пример формата XML

<CommitRequest>
  <commitInstances>
      <instance id="sales$Order-9873c8a8-d4e7-11e2-85c0-33423bc08c84">
          <field name="date">2015-01-30</field>
          <field name="amount">3500.00</field>
          <reference name="customer" id="sales$Customer-32261b09-b7f7-4b8c-88cc-6dee6fa8e6ab"/>
      </instance>
  </commitInstances>
  <removeInstances>
      <instance id="sales$Customer-d67c10f0-4d28-4904-afca-4bc45654985d"/>
  </removeInstances>
  <softDeletion>true</softDeletion>
</CommitRequest>                 

Семантика полей XML-документа определяется в схеме http://schemas.haulmont.com/cuba/5.6/restapi-commit-v2.xsd.

В случае запроса в формате XML установка поля в null осуществляется с помощью атрибута null="true". Кроме того, в идентификаторе должно быть указано представление, содержащее данный атрибут. Например:

<CommitRequest>
    <commitInstances>
        <instance id="Order-9873c8a8-d4e7-11e2-85c0-33423bc08c84">
            <field name="amount" null="true"/>
            <reference name="customer" null="true"/>
        </instance>
    </commitInstances>
</CommitRequest>                        

Функция вызывается посредством POST запроса по адресу {host:port}/app-portal/api/commit?s=<sessionId>. XML передается в теле запроса. Запрос возвращает массив объектов XML вида

<instances>
   <instance ...>
   <instance ...>
</instances>              

Схема, содержащая описание результата вызова функции, находится по адресу http://schemas.haulmont.com/cuba/5.6/restapi-instance-v2.xsd

4.6.2.2.6. Загрузка файла из хранилища

Для загрузки файла из FileStorage необходимо выполнить GET запрос {host:port}/app-portal/api/download?f=<fileDescriptorId>&s=<sessionId> с параметрами:

  • f − идентификатор соответствующего экземпляра FileDescriptor.

  • s − идентификатор текущей сессии.

4.6.2.2.7. Получение описания модели данных в формате HTML

Обращение GET по адресу /printDomain?s=<sessionId> позволяет разработчику получить описание модели данных. Сервис возвращает простой HTML, содержайщий список имен базовых типов данных, описание всех сущностей метамодели, их атрибутов и определенных для сущностей представлений.

4.6.2.2.8. Cоздание новых представлений на сервере

Запрос POST по адресу /deployViews?s=<sessionId> позволяет загрузить на сервер нужные клиенту определения объектов-представлений. Объекты-представления отсылаются в виде стандартного xml-описания представления, используемого в платформе. XML помещается в тело запроса. Подробнее о формате см. Раздел 4.2.3, «Представления»

4.6.2.2.9. Вызов сервисов

Доступные для вызова методы сервисов перечислены в конфигурационном файле, имя которого задается свойством cuba.restServicesConfig.

Пример файла конфигурации сервисов для REST API:

<services xmlns="http://schemas.haulmont.com/cuba/restapi-service-v2.xsd">
   <service name="refapp_PortalTestService">
      <method name="findAllCars"/>
      <method name="updateCarVin"/>
   </service>
</services>

Вызов метода сервиса можно осуществить как с помощью GET, так и с помощью POST запроса. POST запрос дополнительно позволяет передавать сущность или коллекцию сущностей в вызываемый метод.

4.6.2.2.9.1. Вызов сервиса с помощью GET запроса

Формат запроса:

{host:port}/app-portal/api/service.<format>?service=<serviceName>&method=<methodName>&view=<view>&param0=<value 0>&paramN=<value N>&param0_type=<type 0>&paramN_type=<type N>&s=<sessionId>
  • format - задает формат вывода результата. Принимает два значения: xml или json.
  • service - имя вызываемого сервиса.
  • method - имя вызываемого метода.
  • param0 .. paramN - значения параметров метода.
  • param0_type .. paramN_type - типы параметров метода.
  • s - идентификатор текущей сессии

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

4.6.2.2.9.2. Вызов сервиса с помощью POST запроса

Формат запроса:

{host:port}/app-portal/api/service?s=<sessionId>
  • s - идентификатор текущей сессии.

В теле запроса передается JSON или XML с описанием вызова метода.

Формат JSON

В качестве заголовка Content-Type следует использовать значение application/json.

{
"service": "refapp_PortalTestService",
"method": "updateCarVin",
"view": "carEdit",
"params": {
    "param0": {
        "id": "ref$Car-32261b09-b7f7-4b8c-88cc-6dee6fa8e6ab",
        "vin": "WV00001",
        "colour" : {
            "id": "ref$Colour-b32a6412-d4d9-11e2-a20b-87b22b1460c7",
            "name": "Red"
        },
        "driverAllocations": [
            {
                "id": "ref$DriverAllocation-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff"
            },
            {
                "id": "NEW-ref$DriverAllocation"
            }
        ]
    },
        "param1": "WV00001",
        "param0_type": "com.haulmont.refapp.core.entity.Car",
        "param1_type": "java.lang.String"
        }
    }
                          

Свойства передаваемого объекта:

  • service - имя вызываемого сервиса.
  • method - имя вызываемого метода.
  • param0 .. paramN - значения параметров метода.
  • param0_type .. paramN_type - типы параметров метода.
Формат XML

В качестве заголовка Content-Type следует использовать значение text/xml.

<ServiceRequest xmlns="http://schemas.haulmont.com/cuba/restapi-service-v2.xsd">
    <service>refapp_PortalTestService</service>
    <method>updateCarVin</method>
    <view>carEdit</view>
    <params>
        <param name="param0">
            <instance id="ref$Car-32261b09-b7f7-4b8c-88cc-6dee6fa8e6ab">
                <field name="vin">WV00000</field>
                <reference name="colour">
                    <instance id="ref$Colour-b32a6412-d4d9-11e2-a20b-87b22b1460c7">
                        <field name="name">Red</field>
                    </instance>
                </reference>
                <collection name="driverAllocations">
                    <instance id="ref$DriverAllocation-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff"/>
                    <instance id="NEW-ref$DriverAllocation"/>
                </collection>
            </instance>
        </param>
        <param name="param1">WV00001</param>
        <param name="param0_type">com.haulmont.refapp.core.entity.Car</param>
        <param name="param1_type">java.lang.String</param>
        </params>
</ServiceRequest>
                          

Основные элементы передаваемого документа:

  • service - имя вызываемого сервиса.
  • method - имя вызываемого метода.
  • param - значение параметра метода или тип параметра. Имя параметра (атрибут name) должно быть вида param0 .. paramN или param0_type .. paramN_type.

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

Элемент <param> может содержать в себе как текст (для задания значений простых типов данных), так и вложенный элемент <instance> для сущности или <instances> для коллекции сущностей.

XSD запроса доступна по адресу http://schemas.haulmont.com/cuba/5.6/restapi-service-v2.xsd

4.6.2.2.9.3. Поддерживаемые типы параметров метода сервиса
  • примитивные типы Java. В качестве имени типа указывается long, int, boolean и т.д.
  • обертки для примитивных типов Java. В качестве имени типа указывается полное имя класса: java.lang.Boolean, java.lang.Integer и т.д.
  • строка (java.lang.String).
  • дата (java.util.Date).
  • UUID (java.util.UUID).
  • BigDecimal (java.math.BigDecimal).
4.6.2.2.9.4. Результат вызова сервиса

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

Пример результата в формате JSON:

Результат имеет простой тип данных:

{
   "result": "10"
}                          

Результатом является сущность:

{
   "result": {
      "id" : "ref$Colour-b32e43e8-d4d9-11e2-8c8b-2b2939d67fff",
	   "name": "Red"
	}
}	                          
Пример результата в формате XML:

Результат имеет простой тип данных:

<result>
        10
</result>                          

Результатом является сущность:

<result>
   <instance id="ref$Colour-b32a6412-d4d9-11e2-a20b-87b22b1460c7">
      <field name="name">Red</field>
   </instance>
</result>                          

XSD результата доступна по адресу http://schemas.haulmont.com/cuba/5.6/restapi-service-v2.xsd

4.7. Механизмы платформы

В данной главе рассматриваются различные опциональные возможности, предоставляемые платформой.

4.7.1. Выполнение задач по расписанию

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

  • Использование стандартного механизма TaskScheduler фреймворка Spring

  • Использование собственного механизма выполнения назначенных заданий

4.7.1.1. Spring TaskScheduler

Данный механизм подробно описан в разделе Task Execution and Scheduling руководства Spring Framework.

TaskScheduler можно использовать для запуска методов произвольных бинов Spring в любом блоке приложения - как на Middleware, так и на клиентском уровне.

Пример конфигурации в файле spring.xml :

<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:task="http://www.springframework.org/schema/task"
     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
      http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

  ...

  <task:scheduled-tasks scheduler="scheduler">
      <task:scheduled ref="sales_Processor" method="someMethod" fixed-rate="60000"/>
      <task:scheduled ref="sales_Processor" method="someOtherMethod" cron="0 0 1 * * MON-FRI"/>
  </task:scheduled-tasks>
</beans>

Здесь объявлены две задачи, запускающие на выполнение методы someMethod() и someOtherMethod() бина sales_Processor. При этом someMethod() запускается с момента старта приложения через фиксированные промежутки времени - 60 сек. Метод someOtherMethod() запускается в соответствии с расписанием, заданным выражением Cron (описание формата таких выражений см. http://quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger ).

Собственно запуск задач выполняет бин типа TaskScheduler, заданный в атрибуте scheduler элемента scheduled-tasks. В данном случае используется бин CubaThreadPoolTaskScheduler с именем scheduler, который сконфигурирован в модулях core и web базового проекта cuba (см. cuba-spring.xml, cuba-web-spring.xml). Этот класс содержит специфическую реализацию, выполняющую очистку SecurityContext в запускаемых на выполнение потоках.

4.7.1.2. Назначенные задания CUBA

Механизм назначенных заданий CUBA предназначен для запуска по расписанию методов произвольных бинов Spring в блоке Middleware. Целью данного механизма и отличием его от вышеупомянутого стандартного механизма Spring Framework являются:

  • возможность конфигурирования заданий во время работы приложения без остановки сервера

  • координация выполнения синглтон-заданий в кластере Middleware, в том числе:

    • надежная защита от одновременного выполнения

    • привязка заданий к серверам по приоритетам

Под синглтон-заданием понимается задача, которая должна выполняться в некоторый момент времени только на одном сервере. Пример - чтение из очереди и отсылка email.

4.7.1.2.1. Регистрация задания

Задания регистрируются в таблице SYS_SCHEDULED_TASK базы данных, соответствующей сущности ScheduledTask. Для работы с заданиями существуют экраны просмотра и редактирования, доступные через меню Администрирование -> Назначенные задания.

Рассмотрим атрибуты задания:

  • Defined by - каким программным объектом реализуется задание. Возможные значения:

    • Bean - задание реализуется методом бина Spring. Дополнительные атрибуты:

      • Bean name - имя бина. Бин отображается в списке и доступен для выбора, только если у него есть интерфейс, содержащий подходящие для вызова из задания методы. Бины без интерфейса не поддерживаются.

      • Method name - метод интерфейса бина для выполнения. Метод должен либо не иметь параметров, либо иметь все параметры типа String.

      • Method parameters - параметры выбранного метода. Поддерживаются только параметры типа String.

    • Class - задание представляет собой класс, реализующий интерфейс java.util.concurrent.Callable. Класс должен иметь открытый конструктор без параметров. Дополнительные атрибуты:

      • Class name - имя класса

    • Script - задание представляет собой скрипт Groovy. Скрипт выполняется через Scripting.runGroovyScript(). Дополнительные атрибуты:

      • Script name - имя скрипта.

  • User name - имя пользователя, от имени которого будет будет выполняться задание. Если не задано, то задание будет выполнено от имени пользователя, указанного в свойстве приложения cuba.jmxUserLogin .

  • Singleton - признак, является ли задание синглтоном, т.е. выполняющимся только на одном сервере системы.

  • Scheduling type - способ планирования задачи:

    • Cron - с помощью Cron-выражения, представляющего собой последовательность из шести полей, разделенных пробелами: секунда, минута, час, день, месяц, день недели. Месяц и день недели могут быть представлены первыми тремя буквами английского названия. Примеры выражений:

      • 0 0 * * * * - начало каждого часа каждого дня.

      • */10 * * * * * - каждые 10 секунд.

      • 0 0 8-10 * * * - в 8, 9 и 10 часов каждого дня.

      • 0 0/30 8-10 * * * - 8:00, 8:30, 9:00, 9:30 и 10 часов каждого дня.

      • 0 0 9-17 * * MON-FRI - каждый час с 9 до 17 по рабочим дням.

      • 0 0 0 7 1 ? - каждое Рождество в полночь.

    • Period - с помощью интервала между выполнениями.

  • Period - период запуска задания в секундах для Scheduling type = Period.

  • Timeout - время в секундах, по истечении которого считается, что задание закончило выполнение, независимо от того, есть ли информация о завершении задания, или нет. Если timeout не задан явно, он принимается равным 3 часам.

  • Start date - дата/время первого запуска для Scheduling type = Period. Если не установлено, то задание запускается сразу при старте сервера. Если установлено, то задание запускается в момент startDate + period * N, где N - целое число.

    Start date имеет смысл указывать только для "нечастых" заданий - раз в 1 час, 1 сутки и т.п.

  • Time frame - в случае заданного Start date или Cron expression определяет временное окно в секундах, в течение которого будет запущено задание, если время startDate + period * N прошло. Если Time frame не задано явно, оно принимается равным period / 2.

    Если Start date не указано, то Time frame не принимается во внимание, т.е. задание будет запущено в любое время после прохождения промежутка времени Period после предыдущего выполнения задания.

  • Permitted servers - список перечисленных через запятую идентификаторов серверов, на которых возможен запуск данного задания. Если список не задан, то задание может выполняться на любом сервере.

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

    Приоритет серверов работает только в случае Scheduling type = Period и не указанного атрибута Start date. Если Start date указан, старт происходит в одно и то же время, и перехват невозможен.

  • Log start - признак регистрации факта запуска задания в таблице SYS_SCHEDULED_EXECUTION, соответствующей сущности ScheduledExecution.

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

  • Log finish - признак регистрации факта завершения задания в таблице SYS_SCHEDULED_EXECUTION, соответствующей сущности ScheduledExecution.

    Если задание является синглтоном, то в текущей реализации регистрация факта завершения производится в любом случае, независимо от данного признака.

  • Description - произвольное текстовое описание задания.

Задание также имеет признак активности, который устанавливается в экране списка заданий. Неактивные задания не запускаются.

4.7.1.2.2. Управление обработкой заданий
  • Для запуска обработки назначенных заданий необходимо перед стартом сервера установить свойство приложения cuba.schedulingActive в значение true.

  • Для оперативного управления обработкой заданий можно использовать JMX-бин app-core.cuba:type=Scheduling, атрибут Active которого запускает/останавливает обработку заданий для текущего сеанса работы сервера. После перезапуска сервера обработка будет запущена только при установленном в true свойстве приложения cuba.schedulingActive.

  • Все изменения в заданиях, сделанные через экраны системы, вступают в силу немедленно для всех серверов кластера.

  • Для удаления старой истории выполнения заданий можно использовать метод removeExecutionHistory() JMX-бина app-core.cuba:type=Scheduling. У него имееется два параметра:

    • age - время в часах, прошедшее после выполнения задания.

    • maxPeriod - максимальный период заданий в часах, выполнения которых надо удалять. Это позволяет удалять только историю "частых" задач, а историю выполняемых, например, раз в сутки и реже, хранить без ограничений.

    Данный метод можно вызывать автоматически, для этого достаточно создать новое задание и установить для него следующие параметры:

    • Bean name - cuba_SchedulingMBean

    • Method name - removeExecutionHistory(String age, String maxPeriod)

    • Method parameters - например age = 72, maxPeriod = 12.

4.7.1.2.3. Особенности реализации
  • Период вызова обработки заданий (метода SchedulingAPI.processScheduledTasks()) задается в cuba-spring.xml и по умолчанию равен 1 сек. Он задает минимальное значение периода запуска задания, которое должно быть в 2 раза больше, т.е. 2 сек. Уменьшать эти времена не рекомендуется.

  • Текущая реализация планировщика основана на синхронизации с помощью блокировки строк в таблице базы данных. Это означает, что при значительной нагрузке БД может не успевать вовремя отвечать планировщику, и необходимо увеличивать период запуска (>1сек), и, соответственно, минимальный период запуска заданий также будет увеличиваться.

  • Синглтон-задания в случае незаданного атрибута Permitted servers выполняются только на мастер-узле кластера (при выполнении прочих условий). Следует иметь в виду, что отдельный сервер вне кластера также является мастером.

  • Задание не запускается, если оно в данный момент не закончило предыдущее выполнение, и не истек указанный Timeout. Для синглтон-заданий в текущей реализации это обеспечивается информацией в базе данных, для не-синглтонов поддерживается таблица статуса выполнения в памяти сервера.

  • Механизм выполнения создает и кэширует пользовательские сессии в соответствии с указанными для заданий User name, либо свойством приложения cuba.jmxUserLogin . Сессия доступна в потоке выполнения запускаемого задания обычным способом - через интерфейс UserSessionSource .

Для нормальной работы синглтон-заданий необходима точная синхронизация серверов Middleware по времени!

4.7.2. Отправка email

Платформа предоставляет средства отправки сообщений электронной почты со следующими возможностями:

  • Синхронная или асинхронная отправка. В случае синхронной отправки вызывающий код ожидает, пока сообщение не будет передано на SMTP сервер. При асинхронной отправке сообщение сохраняется в базе данных, и управление немедленно возвращается вызывающему коду. Отправка производится позже путем вызова из назначенного задания.

  • Надежная фиксация факта отправки и ошибок в базе данных, как для синхронной, так и для асинхронной отправки.

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

4.7.2.1. Методы отправки

Для отправки email на Middleware следует использовать бин EmailerAPI, на клиентском уровне - сервис EmailService.

Рассмотрим основные методы этих компонентов:

  • sendEmail() - синхронная отправка сообщения. Вызывающий код блокируется на время отправки сообщения SMTP серверу.

    Сообщение может быть передано как в виде набора параметров (список адресатов через запятую, тема, содержимое, массив вложений), так и в виде специального объекта EmailInfo, инкапсулирующего всю эту информацию, плюс позволяющего явно задать адрес отправителя и сформировать тело письма по шаблону FreeMarker.

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

    В процессе работы метода для каждого адресата в базе данных создается экземпляр сущности SendingMessage, который сначала получает статус SendingStatus.SENDING, а после успешной отправки - SendingStatus.SENT. В случае ошибки отправки статус сообщения меняется на SendingStatus.NOTSENT.

  • sendEmailAsync() - асинхронная отправка сообщения. Данный метод возвращает список (по числу получателей) экземпляров SendingMessage со статусом SendingStatus.QUEUE, созданных в базе данных. Собственно отправка производится при последующем вызове метода EmailManagerAPI.queueEmailsToSend(), который необходимо зарегистрировать в механизме назначенных заданий с желаемой периодичностью.

4.7.2.2. Вложения

Объект EmailAttachment - обёртка, хранящая вложение в виде массива байт (поле data), имя файла (поле name), и при необходимости, уникальный для данного сообщения идентификатор вложения (необязательное, но полезное поле contentId).

Идентификатор вложения может быть использован для вставки в сообщение изображений следующим образом: при создании EmailAttachment задаётся уникальный contentId, например, myPic. В теле письма для вставки вложения необходимо в качестве пути использовать запись вида: cid:myPic. Т.е. для вставки изображения нужно указать следующий элемент HTML:

<img src="cid:myPic"/>

4.7.2.3. Настройка параметров отправки email

Параметры отправки email могут быть настроены с помощью перечисленных ниже свойств приложения. Все они являются параметрами времени выполнения и хранятся в базе данных, однако могут быть переопределены для конкретного блока Middleware в его файле app.properties.

Все параметры отправки email доступны через конфигурационный интерфейс EmailerConfig.

  • cuba.email.fromAddress - адрес отправителя по умолчанию. Принимается во внимание, если не указан атрибут EmailInfo.from.

    Значение по умолчанию: DoNotReply@localhost

  • cuba.email.smtpHost - адрес SMTP сервера.

    Значение по умолчанию: test.host

  • cuba.email.smtpPort - порт SMTP сервера.

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

  • cuba.email.smtpAuthRequired - требуется ли аутентификация на SMTP сервере. Соответствует параметру mail.smtp.auth, передаваемому при создании объекта javax.mail.Session.

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

  • cuba.email.smtpStarttlsEnable - задает использование команды STARTTLS при аутентификации на SMTP сервере. Соответствует параметру mail.smtp.starttls.enable, передаваемому при создании объекта javax.mail.Session.

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

  • cuba.email.smtpUser - имя пользователя для аутентификации на SMTP сервере.

  • cuba.email.smtpPassword - пароль пользователя для аутентификации на SMTP сервере.

  • cuba.email.delayCallCount - используется при асинхронной отправке email из очереди для пропуска нескольких первых вызовов EmailManager.queueEmailsToSend() сразу после старта сервера, чтобы снизить нагрузку во время инициализации приложения. Отправка email начнется следующим вызовом.

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

  • cuba.email.messageQueueCapacity - при асинхронной отправке количество сообщений, читаемое из очереди и отправляемое за один вызов EmailManager.queueEmailsToSend().

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

  • cuba.email.defaultSendingAttemptsCount - при асинхронной отправке email количество попыток отправки по умолчанию. Принимается во внимание, если при вызове Emailer.sendEmailAsync() не указан параметр attemptsCount.

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

  • cuba.email.maxSendingTimeSec - максимальное предполагаемое время в секундах, требуемое для отправки сообщения на SMTP сервер. Используется при асинхронной отправке для оптимизации выборки объектов SendingMessage из очереди в БД.

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

  • cuba.email.sendAllToAdmin - указывает, что все сообщения должны отправляться на адрес cuba.email.adminAddress, независимо от указанного адреса получателя. Этот параметр рекомендуется использовать во время отладки системы.

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

  • cuba.email.adminAddress - адрес, на который отправляются все сообщения при включенном свойстве cuba.email.sendAllToAdmin.

    Значение по умолчанию: admin@localhost

  • cuba.emailerUserLogin - логин пользователя системы, под которым регистрируется механизм асинхронной отправки email для того, чтобы иметь возможность сохранить информацию в базе данных. Рекомендуется создать отдельного пользователя (например emailer) без пароля, чтобы под его именем нельзя было войти через пользовательский интерфейс приложения. Это полезно для поиска в логе сервера сообщений, касаемых отсылки email.

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

Просмотреть текущие значения параметров, а также отправить тестовое сообщение, можно с помощью JMX-бина app-core.cuba:type=Emailer.

4.7.3. Динамические атрибуты

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

Динамические атрибуты CUBA являются реализацией концепции Entity-Attribute-Value.

Рисунок 31. Диаграмма классов механизма динамических атрибутов

Диаграмма классов механизма динамических атрибутов

  • Category - определяет категорию объектов, которая содержит описание структуры динамических атрибутов. Каждая категория относится к некоторому типу сущности.

    Например, имеется сущность типа Автомобиль. Для нее можно определить две категории: Грузовой и Пассажирский. При этом категория Грузовой будет содержать атрибуты Грузоподъемность и Вид кузова, а категория Пассажирский - атрибуты Количество мест и Наличие детского сидения.

  • CategoryAttribute - определяет динамический атрибут, относящийся к некоторой категории. Каждый атрибут описывает одно поле определенного типа. У каждого атрибута имеется обязательное поле Код (code), которое используется в качестве его системного имени. Имя атрибута (name) используется для отображения пользователю.

  • CategoryAttributeValue - значение динамического атрибута для конкретного экземпляра сущности. Физически значения динамических атрибутов хранятся в специальной таблице SYS_ATTR_VALUE. У каждой записи этой таблицы есть ссылка на определенную сущность (колонка ENTITY_ID).

Экземпляр сущности может иметь атрибуты одновременно из всех категорий, связанных с этим типом сущности. Если необходимо, чтобы некоторый экземпляр сущности принадлежал только одной категории с соответствующим набором атрибутов (Автомобиль может быть либо Грузовым, либо Пассажирским), класс сущности должен реализовывать интерфейс Categorized. В этом случае экземпляр сущности будет содержать ссылку на категорию и динамические атрибуты только выбранной категории.

Загрузка и сохранение динамических атрибутов осуществляется в DataManager. Для указания того, что динамические атрибуты должны быть загружены вместе с экземплярами сущностей, используется метод LoadContext.setLoadDynamicAttributes(). По умолчанию динамические атрибуты не загружаются. В то же время DataManager всегда сохраняет динамические атрибуты, содержащиеся в экземплярах сущностей, переданных в commit().

Доступ к значениям динамических атрибутов может быть осуществлен через методы getValue() / setValue() любой персистентной сущности, унаследованной от BaseGenericIdEntity. В эти методы необходимо передавать код атрибута с префиксом +, например:

LoadContext lc = new LoadContext(Car.class).setId(id);
lc.setLoadDynamicAttributes(true);
Entity entity = dataManager.load(lc);

Double capacity = entity.getValue("+loadCapacity");
entity.setValue("+loadCapacity", capacity + 10);

dataManager.commit(entity);

На самом деле, прямой доступ к значениям динамических атрибутов в коде приложения нужен крайне редко. Любой динамический атрибут может быть автоматически отображен в любом компоненте Table или FieldGroup, связанном с источником данных, содержащим сущность, для которой данный атрибут был создан. Экран редактирования атрибута позволяет указать, в каких экранах и компонентах отображать атрибут.

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

4.7.3.1. Управление динамическими атрибутами

Управление категориями и описаниями атрибутов осуществляется с помощью специальных экранов, доступных через меню Administration > Categories.

Рисунок 32. Экран списка категорий

Экран списка категорий

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

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

Экран редактирования категории

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

Рисунок 34. Редактор динамического атрибута

Редактор динамического атрибута

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

Рисунок 35. Настройки видимости динамического атрибута

Настройки видимости динамического атрибута

Кроме экрана можно также указать компонент, в котором атрибут должен появляться (например, для экранов, где несколько компонентов FieldGroup показывают поля одной и той же сущности).

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

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

Для того чтобы изменения в атрибутах и настройках видимости вступили в силу, необходимо нажать кнопку Применить изменения на экране со списком категорий. Изменения также можно применить через Administration > JMX Console, вызвав метод clearDynamicAttributesCache() JMX бина app-core.cuba:type=CachingFacade.

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

Динамические атрибуты можно добавить в экран вручную. Для этого необходимо выполнить следующее:

  • В секции dsContext XML-дескриптора экрана для источника данных с загружаемой сущностью (сущностями) установить в true признак loadDynamicAttributes для источника данных с загружаемой сущностью (сущностями), например:

    <dsContext>
      <datasource id="carDs" class="com.company.sample.entity.Car" view="_local" loadDynamicAttributes="true"/>
    </dsContext>
  • В описании визуального компонента в качестве property нужно использовать код динамического атрибута с префиксом +:

    <textField id="numberOfSeats" datasource="carDs" property="+numberOfSeats"/>

4.7.3.2. Категоризируемые сущности

Если сущность реализует интерфейс com.haulmont.cuba.core.entity.Categorized, то для работы с ее динамическими атрибутами можно использовать компонент com.haulmont.cuba.gui.components.RuntimePropertiesFrame. Этот компонент позволяет пользователю выбрать для экземпляра сущности некоторую категорию и указать значения динамических атрибутов этой категории.

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

  • В секции dsContext необходимо объявить два источника данных:

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

    • обычный collectionDatasource для загрузки списка категорий данного типа сущности.

    Например:

    <dsContext>
      <datasource id="carDs"
          class="com.company.sample.entity.Car"
          view="carEdit"/>
    
      <runtimePropsDatasource id="runtimePropsDs"
          mainDs="carDs"/>
    
      <collectionDatasource id="categories"
          class="com.haulmont.cuba.core.entity.Category"
          view="_local">
        <query>
             select c from sys$Category c where c.entityType='sample$Car'
        </query>
      </collectionDatasource>
    </dsContext>

  • После этого можно включить в XML-дескриптор экрана визуальный компонент runtimeProperties:

    <runtimeProperties id="runtimePropsFrame"
      runtimeDs="runtimePropsDs"
      categoriesDs="categories"/>

4.7.3.3. Динамические атрибуты в REST API

REST API поддерживает загрузку и сохранение динамических атрибутов. Для этого достаточно добавить в URL запроса find or query параметр dynamicAttributes=true:

app-portal/api/find.xml?e=sample$Car-9f789ba9-ca15-4758-a8b8-77e434f1d438&s=9f789ba9-ca15-4758-a8b8-77e434f1d438&dynamicAttributes=true
      

Динамические атрибуты в JSON и XML документах представляются так же как и обычные атрибуты, с той разницей, что имя динамического атрибута представляет собой его код с префиксом +.

REST API также сохраняет динамические атрибуты, переданные в commit.

4.7.4. Пессимистичная блокировка

В данном разделе описано применение пессимистичной блокировки в CUBA-приложениях.

4.7.4.1. Блокировка редактирования сущностей

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

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

Режим пессимистичной блокировки может быть задан для любого класса сущности в процессе настройки или эксплуатации системы. Для этого достаточно выполнить следующие действия:

  • вставить в таблицу SYS_LOCK_CONFIG запись со следующими значениями полей:

    • ID - произвольный идентификатор типа UUID.

    • NAME - наименование блокируемого объекта. Для сущности это должно быть имя ее мета-класса.

    • TIMEOUT_SEC - таймаут истечения блокировки в секундах.

    Например:

    insert into sys_lock_config (id, create_ts, name, timeout_sec) values (newid(), current_timestamp, 'sales$Order', 300)
  • перезапустить сервер или выполнить метод reloadConfiguration() JMX-бина app-core.cuba:type=LockManager.

4.7.4.2. Блокировка произвольных процессов

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

Подробнее см. JavaDoc интерфейсов LockManagerAPI и LockService.

4.7.4.3. Мониторинг блокировок

Текущее состояние блокировок можно отслеживать через JMX-бин app-core.cuba:type=LockManager, или через специальный экран, доступный в меню Администрирование -> Блокировки. Экран также позволяет разблокировать любой объект принудительно.

4.7.5. Статистика сущностей

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

Статистика хранится в таблице SYS_ENTITY_STATISTICS, соответствующей сущности EntityStatistics. Заполнить статистику можно как вручную, внося соответствующие записи в таблицу, так и автоматически с помощью метода refreshStatistics() JMX-бина PersistenceManagerMBean . При указании в качестве параметра имени сущности статистика будет собрана только для данной сущности, в противном случае - для всех. Сбор статистики может занять значительное время и вызвать нежелательную нагрузку на БД, поэтому выполнять его нужно либо вручную, либо назначенным заданием в подходящее время.

Программный доступ к статистике осуществляется с помощью интерфейса PersistenceManagerAPI на Middleware и PersistenceManagerService на клиентском уровне. Статистика кэшируется в памяти, поэтому если изменения статистики вносятся напрямую в базу данных, для вступления их в силу необходимо перезапустить сервер или вызвать метод PersistenceManagerMBean.flushStatisticsCache().

Рассмотрим атрибуты EntityStatistics и их влияние на поведение системы.

  • name (колонка NAME) - тип сущности в виде имени мета-класса, например, sales$Customer.

  • instanceCount (колонка INSTANCE_COUNT) - примерное текущее количество экземпляров сущности.

  • fetchUI (колонка FETCH_UI) - размер страницы данных, предлагаемый пользователю при извлечении списков сущностей.

    Например, компонент Filter устанавливает это число в поле Показывать N строк.

  • maxFetchUI (колонка MAX_FETCH_UI) - максимальное количество экземпляров сущности, которое может быть извлечено и передано на клиентский уровень.

    Данный параметр играет роль при отображении списков сущностей в компонентах типа LookupField и LookupPickerField , а также в таблицах без универсального фильтра, то есть когда на связанный источник данных не налагается ограничений методом CollectionDatasource.setMaxResults(). В этом случае сам источник данных ограничивает количество извлекаемых экземпляров значением maxFetchUI.

  • lookupScreenThreshold (колонка LOOKUP_SCREEN_THRESHOLD) - порог количества экземпляров сущности, при превышении которого в универсальных механизмах пользовательского интерфейса для поиска связанных сущностей будут использоваться экраны выбора вместо выпадающих списков.

    В частности, этот параметр принимается во внимание компонентом Filter при выборе параметров фильтрации: до достижения порога используется компонент LookupField , при превышении порога - компонент PickerField . Поэтому, если необходимо заставить фильтр отображать выбор параметра некоторого типа через экран выбора, достаточно внести запись статистики для этой сущности со значением lookupScreenThreshold меньшим, чем instanceCount.

JMX-бин PersistenceManagerMBean в атрибутах DefaultFetchUI, DefaultMaxFetchUI, DefaultLookupScreenThreshold позволяет задать значения вышеперечисленных параметров по умолчанию. В результате, если для некоторой сущности статистика отсутствует (что является обычной ситуацией), будет использоваться соответствующий параметр по умолчанию.

Кроме того, JMX-бин PersistenceManagerMBean позволяет ввести данные статистики для конкретной сущности с помощью операции enterStatistics(). Например, для того, чтобы для сущности sales$Customer установить размер страницы данных по умолчанию в 1000, а максимальное количество извлекаемых экземпляров в компонентах LookupField в 30000, следует вызвать операцию enterStatistics() со следующими параметрами:

entityName: sales$Customer
fetchUI: 1000
maxFetchUI: 30000

4.7.6. Журнал изменений сущностей

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

Данный механизм перехватывает сохранение сущностей в БД на уровне Entity Listeners, т.е. гарантированно отслеживаются все изменения, проходящие через персистентный контекст EntityManager . Непосредственное изменение сущностей в базе данных с помощью SQL, в том числе изнутри системы через NativeQuery и QueryRunner , в журнал не попадает.

Измененные экземпляры сущностей перед сохранением в БД отправляются в методы registerCreate(), registerModify(), registerDelete() бина EntityLogAPI. Параметр auto этих методов позволяет отделить автоматическое журналирование посредством Entity Listeners от ручного вызова этих же методов в прикладном коде. При вызове из Entity Listeners в параметре auto передается true.

Журнал содержит информация о том, кто и когда изменил данный экземпляр, а также новые значения измененных атрибутов. Записи журнала сохраняются в таблице SEC_ENTITY_LOG базы данных, соответствующей сущности EntityLogItem. Измененные значения атрибутов хранятся в этой же таблице в колонке CHANGES, а при чтении на Middleware преобразуются в экземпляры сущности EntityLogAttr.

4.7.6.1. Настройка журналирования

Аудит настраивается при помощи сущностей LoggedEntity и LoggedAttribute (соответствующих таблицам SEC_LOGGED_ENTITY и SEC_LOGGED_ATTR).

LoggedEntity описывает тип сущности, изменения которой необходимо журналировать. Атрибуты LoggedEntity:

  • name (колонка NAME) - тип сущности в виде имени мета-класса, например, sales$Customer.

  • auto (колонка AUTO) - нужно ли журналировать изменения при вызове EntityLogAPI с параметром auto = true (т.е. из Entity Listeners).

  • manual (колонка MANUAL) - нужно ли журналировать изменения при вызове EntityLogAPI с параметром auto = false.

LoggedAttribute описывает журналируемый атрибут сущности и содержит ссылку на LoggedEntity и имя атрибута.

Для настройки журналирования некоторой сущности достаточно внести соответствующие записи в таблицы SEC_LOGGED_ENTITY и SEC_LOGGED_ATTR. Например, для ведения журнала изменений атрибутов name и grade сущности Customer, необходимо выполнить:

insert into SEC_LOGGED_ENTITY (ID, CREATE_TS, CREATED_BY, NAME, AUTO, MANUAL)
values ('25eeb644-e609-11e1-9ada-3860770d7eaf', now(), 'admin', 'sales$Customer', true, true);

insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'name');

insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'grade');

Для активации механизма журналирования необходимо установить в true атрибут Enabled JMX-бина app-core.cuba:type=EntityLog. Для вступления в силу изменений настройки журналирования, произведенных во время работы приложения, необходимо перезапустить сервер или вызвать метод invalidateCache() этого же бина.

4.7.6.2. Отображение журнала

Для просмотра журнала изменений некоторого экземпляра сущности достаточно обычным способом загрузить в источники данных экрана коллекцию экземпляров EntityLogItem и ассоциированных с ними EntityLogAttr, и создать визуальные компоненты, связанные с этими источниками. Например:

<dsContext>
  <datasource id="customerDs"
              class="com.sample.sales.entity.Customer"
              view="customerEdit"/>

  <collectionDatasource id="logDs"
                        class="com.haulmont.cuba.security.entity.EntityLogItem"
                        view="logView">
      <query>
          select i from sec$EntityLog i
          where i.entityId = :ds$customerDs order by i.eventTs
      </query>

      <collectionDatasource id="logAttrDs"
                            property="attributes"/>
  </collectionDatasource>
</dsContext>
<layout>
...
  <split orientation="vertical" width="100%" height="100%">

      <table id="logTable" width="100%" height="100%">
          <columns>
              <column id="eventTs"/>
              <column id="user.login"/>
              <column id="type"/>
          </columns>
          <rows datasource="logDs"/>
      </table>

      <table id="logAttrTable" width="100%" height="100%">
          <columns>
              <column id="name"/>
              <column id="value"/>
          </columns>
          <rows datasource="logAttrDs"/>
      </table>

  </split>
...
</layout>

Для отображения локализованных значений журналируемых атрибутов эти атрибуты должны содержать аннотацию @LocalizedValue . При ее наличии механизм журналирования заполняет поле EntityLogAttr.messagesPack, и таблица, отображающая значения атрибутов из примера выше может использовать колонку locValue вместо value:

<table id="logAttrTable" width="100%" height="100%">
  <columns>
      <column id="name"/>
      <column id="locValue"/>
  </columns>
  <rows datasource="logAttrDs"/>
</table>

4.7.7. Снимки сущностей

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

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

  • Процесс сохранения снимка вызывается явно из кода клиентского уровня.

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

4.7.7.1. Сохранение снимков

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

Граф Java объектов преобразуется в XML и сохраняется в базе данных вместе со ссылкой на основную сущность в таблице SYS_ENTITY_SNAPSHOT, соответствующей сущности EntitySnapshot.

Как правило, снимки требуется сохранять после коммита экрана редактирования. Для этого можно переопределить метод postCommit() контроллера экрана, например:

public class CustomerEditor extends AbstractEditor<Customer> {

  @Inject
  protected Datasource<Customer> customerDs;

  @Inject
  protected EntitySnapshotService entitySnapshotService;

...
  @Override
  protected boolean postCommit(boolean committed, boolean close) {
      if (committed) {
          entitySnapshotService.createSnapshot(customerDs.getItem(), customerDs.getView());
      }
      return super.postCommit(committed, close);
  }
}

4.7.7.2. Отображение снимков

Для отображения сохраненных для некоторой сущности снимков можно использовать фрейм com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml, например:

<iframe id="diffFrame"
      src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"
      width="100%"
      height="100%"/>

В контроллере экрана редактирования необходимо вызвать загрузку снимков во фрейм:

public class CustomerEditor extends AbstractEditor<Customer> {

  @Inject
  protected EntityDiffViewer diffFrame;

...
  @Override
  protected void postInit() {
      if (!PersistenceHelper.isNew(getItem())) {
          diffFrame.loadVersions(getItem());
      }
  }
}

Фрейм diff-view.xml отображает список сохраненных для данной сущности снимков с возможностью их сравнения. Для каждого снимка указывается пользователь, дата и время сохранения. При выборе из списка некоторого снимка сущности в таблице сравнения показываются изменения данных по сравнению с предыдущим снимком. В первом снимке измененными считаются все атрибуты. Если выбрано два снимка, то в таблицу выводится результат их сравнения.

В таблице сравнения отображаются имена атрибутов и их новые значения, при выборе строки показывается детальная информация по изменениям атрибута в двух снимках. Ссылочные поля выводятся в соответствии с их шаблоном @NamePattern . При сравнении коллекций добавленные и удаленные элементы выделяются цветом (зеленый, красный), а элементы с измененными атрибутами остаются без выделения. Изменение позиций элементов не учитывается.

4.7.8. Хранилище файлов

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

Механизм работы с файлами состоит из следующих частей:

  • Сущность FileDescriptor - описатель загруженного файла (не путать с java.io.FileDescriptor), позволяющий ссылаться на файл из объектов модели данных.

  • Интерфейс FileStorageAPI - доступ к хранилищу файлов на уровне Middleware. Основные методы:

    • saveStream() - сохранить содержимое файла, переданное в InputStream, по данным указанного FileDescriptor.

    • openStream() - вернуть содержимое файла, указанного объектом FileDescriptor, в виде открытого InputStream.

  • Класс FileUploadController - контроллер Spring MVC, позволяющий отправлять файлы с клиентского уровня на Middleware посредством HTTP POST запросов.

  • Класс FileDownloadController - контроллер Spring MVC, позволяющий получать файлы с Middleware на клиентский уровень посредством HTTP GET запросов.

  • Визуальные компоненты FileUpload и FileMultiUpload - позволяют загрузить файлы с компьютера пользователя на клиентский уровень приложения, и затем организовать их передачу на Middleware.

  • Интерфейс FileUploadingAPI - промежуточное хранилище загружаемых файлов на клиентском уровне. Используется вышеупомянутыми компонентами для загрузки файлов на клиентский уровень. В прикладном коде используется метод putFileIntoStorage(), перемещающий файл в постоянное хранилище на Middleware.

  • ExportDisplay - интерфейс клиентского уровня, позволяющий выгружать различные ресурсы приложения на компьютер пользователя. Для получения файлов из хранилища можно использовать метод show(), принимающий FileDescriptor. Экземпляр ExportDisplay можно получить либо вызовом статического метода AppConfig.createExportDisplay(), либо инжекцией в класс контроллера.

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

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

Для загрузки файлов с компьютера пользователя в хранилище следует использовать компоненты FileUpload и FileMultiUpload . Примеры использования приведены в описании компонентов.

Промежуточное хранилище клиентского уровня FileUploadingAPI для хранения временных файлов использует каталог, заданный свойством приложения cuba.tempDir . В случае сбоев в нем могут оставаться временные файлы, для удаления которых желательно в клиентских блоках приложения периодически вызывать метод clearTempDirectory() бина cuba_FileUploading. Это можно сделать, создав задание планировщика в файле spring.xml модуля web (и/или desktop) проекта приложения, например:

<task:scheduled-tasks scheduler="scheduler">
  <task:scheduled ref="cuba_FileUploading" method="clearTempDirectory" cron="0 0 0 * * 2,4,6"/>
</task:scheduled-tasks>

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

4.7.8.2. Выгрузка данных

Для выгрузки файлов на клиентском уровне следует использовать интерфейс ExportDisplay, получив ссылку на него вызовом статического метода AppConfig.createExportDisplay(), либо инжекцией в класс контроллера. Например:

AppConfig.createExportDisplay(this).show(fileDescriptor);

Метод show() может принимать дополнительный параметр типа ExportFormat, в котором можно задать тип содержимого и расширение имени файла. Если формат не передан, расширение берется из FileDescriptor, а типом содержимого принимается application/octet-stream.

При использовании пользователем веб-интерфейса от расширения имени файла зависит, будет ли файл выгружаться через диалог сохранения или открытия файлов браузера (Content-Disposition = attachment), или браузер попытается отобразить содержимое прямо в своем окне (Content-Disposition = inline). Список расширений файлов, отображаемых в окне браузера, задается свойством приложения cuba.web.viewFileExtensions .

4.7.8.3. Стандартная реализация хранилища

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

Корни структуры можно задать в свойстве приложения cuba.fileStorageDir . Формат - список путей через запятую. Например:

cuba.fileStorageDir=/work/sales/filestorage,/mnt/backup/filestorage

Если данное свойство не задано, хранилище будет создано в подкаталоге filestorage рабочего каталога Middleware. В стандартном варианте развертывания в Tomcat это каталог tomcat/work/app-core/filestorage.

В случае указания нескольких ресурсов хранилище ведет себя следующим образом:

  • Первый каталог в списке является основным, остальные - резервными.

  • Запись сохраняемых файлов производится в основной каталог, а затем файл копируется во все резервные каталоги.

    Перед записью проверяется доступность каждого каталога. Если недоступен основной каталог, выбрасывается исключение и запись не производится. Если недоступен какой-то из резервных каталогов, запись все равно производится, в лог выводится сообщение об ошибке.

  • Чтение производится из основного каталога.

    При недоступности основного каталога чтение производится из первого резервного каталога, в котором имеется данный файл. В лог выводится сообщение об ошибке.

Файловая структура хранилища организована следующим образом:

  • Имеется три уровня каталогов, соответствующих дате загрузки файла - год, месяц, день.

  • Файл сохраняется в каталоге дня. Именем файла является идентификатор соответствующего объекта FileDescriptor. Расширение файла - исходное.

  • В корне структуры хранилища ведется файл storage.log, содержащий информацию о том, какой файл, когда и каким пользователем был записан в хранилище. Этот журнал не несет никакой функциональности, но может быть полезен при поиске проблем.

JMX-бин app-core.cuba:type=FileStorage отображает текущий список корней хранилища, а также предоставляет следующие методы для поиска проблем:

  • findOrphanDescriptors() - найти в базе данных все экземпляры FileDescriptor, для которых не имеется соответствующего файла в хранилище.

  • findOrphanFiles() - найти файлы в хранилище, для которых не имеется соответствующего экземпляра FileDescriptor в БД.

4.7.9. Генерация последовательностей

Данный механизм позволяет генерировать уникальные последовательности чисел через единый API, независимо от используемой СУБД.

Основной частью данного механизма является бин UniqueNumbers с интерфейсом UniqueNumbersAPI, доступный в блоке Middleware. Методы интерфейса:

  • getNextNumber() - получить следующее значение последовательности. Механизм позволяет вести одновременно несколько последовательностей, идентифицируемых простыми строками. Имя последовательности, из которой нужно получить значение, передается в параметре domain.

    Последовательности не требуют предварительной инициализации - при первом вызове getNextNumber() соответствующая последовательность будет создана и вернет значение 1.

  • getCurrentNumber() - получить текущее, то есть последнее сгенерированное, значение последовательности. Параметр domain - имя последовательности.

  • setCurrentNumber() - установить текущее значение последовательности. Следующий вызов getNextNumber() вернет значение, увеличенное на 1.

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

@Inject
private UniqueNumbersAPI uniqueNumbers;

private long getNextValue() {
  return uniqueNumbers.getNextNumber("mySequence");
}

Для получения значений последовательностей в клиентских блоках используется метод getNextNumber() сервиса UniqueNumbersService.

Для управления последовательностями можно использовать JMX-бин app-core.cuba:type=UniqueNumbers с методами, дублирующими методы UniqueNumbersAPI.

Реализация механизма генерации последовательностей зависит от типа используемой СУБД. Для HSQL, PostgreSQL, Microsoft SQL Server 2012+ и Oracle каждой последовательности UniqueNumbersAPI соответствует последовательность (sequence) sec_un_{domain} в базе данных. Для Microsoft SQL Server версии ниже 2012 каждой последовательности соответствует таблица sec_un_{domain} с автоинкрементным полем. В связи с этим управлять параметрами последовательности можно также напрямую в БД.

4.7.10. Выполнение SQL с помощью QueryRunner

QueryRunner - класс, предназначенный для выполнения SQL. Его следует использовать вместо JDBC везде, где есть необходимость работы с SQL и нежелательно применение аналогичных средств ORM.

QueryRunner платформы является вариантом Apache DbUtils QueryRunner, усовершенствованным для использования Java Generics.

Пример использования:


QueryRunner runner = new QueryRunner(persistence.getDataSource());
try {
  Set<String> scripts = runner.query("select SCRIPT_NAME from SYS_DB_CHANGELOG",
          new ResultSetHandler<Set<String>>() {
              public Set<String> handle(ResultSet rs) throws SQLException {
                  Set<String> rows = new HashSet<String>();
                  while (rs.next()) {
                      rows.add(rs.getString(1));
                  }
                  return rows;
              }
          });
  return scripts;
} catch (SQLException e) {
  throw new RuntimeException(e);
}

Есть два варианта использования QueryRunner - либо в текущей транзакции, либо в отдельной в режиме autocommit.

  • Для выполнения запроса в текущей транзакции необходимо создать экземпляр QueryRunner конструктором без параметров, не передавая DataSource. После этого нужно вызывать методы query() или update(), передавая в них Connection, полученный вызовом EntityManager.getConnection(). После выполнения закрывать Connection не нужно, он будет закрыт при коммите транзакции.

  • Для выполнения запроса в отдельной транзакции необходимо создать экземпляр QueryRunner конструктором с параметром DataSource, получив экземпляр DataSource вызовом Persistence.getDataSource(). После этого нужно вызывать методы query() или update() без передачи какого-либо Connection, оно будет создано из указанного DataSource и затем сразу закрыто.

4.7.11. Интеграция с MyBatis

В состав платформы включен фреймворк MyBatis, обладающий, по сравнению с ORM и QueryRunner, более широкими возможностями по выполнению SQL и отображению результатов на объекты предметной области.

Для использование MyBatis в проекте необходимо добавить следующие бины в файл spring.xml модуля core:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="configLocation" value="cuba-mybatis.xml"/>
  <property name="mapperLocations" value="classpath*:com/sample/sales/core/sqlmap/*.xml"/>
</bean>

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

В параметре mapperLocations задается путь (по правилам интерфейса ResourceLoader Spring) к файлам отображений MyBatis.

Пример файла отображения для загрузки экземпляра сущности Заказ вместе со связанным Покупателем и коллекцией Пунктов заказа:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sample.sales">

  <select id="selectOrder" resultMap="orderResultMap">
      select
          o.ID as order_id,
          o.DATE as order_date,
          o.AMOUNT as order_amount,
          c.ID as customer_id,
          c.NAME as customer_name,
          c.EMAIL as customer_email,
          i.ID as item_id,
          i.QUANTITY as item_quantity,
          p.ID as product_id,
          p.NAME as product_name
      from
          SALES_ORDER o
          left join SALES_CUSTOMER c on c.ID = o.CUSTOMER_ID
          left join SALES_ITEM i on i.ORDER_ID = o.id and i.DELETE_TS is null
          left join SALES_PRODUCT p on p.ID = i.PRODUCT_ID
      where
          c.id = #{id}
  </select>

  <resultMap id="orderResultMap" type="com.sample.sales.entity.Order">
      <id property="id" column="order_id"/>
      <result property="date" column="order_date"/>
      <result property="amount" column="order_amount"/>

      <association property="customer" column="customer_id" javaType="com.sample.sales.entity.Customer">
          <id property="id" column="customer_id"/>
          <result property="name" column="customer_name"/>
          <result property="email" column="customer_email"/>
      </association>

      <collection property="items" ofType="com.sample.sales.entity.Item">
          <id property="id" column="item_id"/>
          <result property="quantity" column="item_quantity"/>
          <association property="product" column="product_id" javaType="com.sample.sales.entity.Product">
              <id property="id" column="product_id"/>
              <result property="name" column="product_name"/>
          </association>
      </collection>
  </resultMap>

</mapper>

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

Transaction tx = persistence.createTransaction();
try {
  SqlSession sqlSession = AppBeans.get("sqlSession");
  Order order = (Order) sqlSession.selectOne("com.sample.sales.selectOrder", orderId);
  tx.commit();
} finally {
  tx.end();
}

Объекты, загруженные с помощью MyBatis, можно изменять и передавать в EntityManager.merge() для сохранения в базе данных. При этом в апдейт будут включены только не-null атрибуты, то есть если атрибут не был загружен, или специально установлен в значение null, соответствующее поле в БД не будет изменено.

Такое поведение определяется параметром ORM openjpa.DetachState=loaded, установленным по умолчанию.

4.7.12. Панель папок

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

На момент написания данного руководства панель папок реализована только для Web Client .

Платформа поддерживает три вида папок: папки приложения, папки поиска и наборы записей. Папки приложения отображаются в верхней части панели в отдельной иерархии, папки поиска и наборы - в нижней части панели в совместной иерархии.

  • Папки приложения:

    • Открывают экраны с фильтром или без него.

    • Набор папок может зависеть от текущего сеанса пользователя. Видимость конкретной папки определяется путем выполнения скрипта Groovy.

    • Пользователь может создавать или изменять папки приложения, только если у него есть специальное право.

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

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

  • Папки поиска:

    • Открывают экраны с фильтром.

    • Могут быть как локальными - доступными только пользователю, их создавшему, так и глобальными - доступными всем пользователям.

    • Локальные папки может создавать и изменять любой пользователь, глобальные - только имеющий специальное право.

  • Наборы:

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

    • Содержимое набора редактируется с помощью специальных действий таблицы: Добавить в набор, Удалить из набора.

    • Наборы локальны, то есть доступны только создавшему их пользователю.

На функционирование панели папок влияют следующие свойства приложения:

4.7.12.1. Папки приложения

Для создания папок приложения пользователь должен иметь специфическое право Создание/изменение папок приложения (код cuba.gui.appFolder.global).

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

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

  • Открыть экран и отобрать записи по нужному фильтру.

  • В меню кнопки Фильтр... выбрать команду Сохранить как папку приложения.

  • В окне добавления заполнить атрибуты папки:

    • Наименование папки

    • Заголовок окна - строка, добавляемая к заголовку окна, когда он открывается из папки

    • Родительская папка - определяет место создаваемой папки в иерархии

    • Скрипт видимости - скрипт Groovy, выполняемый в начале сеанса пользователя, и определяющий доступность для него данной папки.

      Скрипт должен вернуть булевское значение. Если скрипт не задан, либо возвращает null, папка доступна. Пример:

      userSession.currentOrSubstitutedUser.login == 'admin'
    • Cкрипт количества - скрипт Groovy, выполняемый в начале сеанса пользователя и по таймеру, для вычисления количества записей для данной папки и ее стиля отображения.

      Скрипт должен вернуть числовое значение, целая часть которого будет использована в качестве счетчика. Если скрипт не задан, либо возвращает null, счетчик не будет отображаться. Кроме возвращаемого значения скрипт может установить переменную style, которая будет использована как имя стиля отображения папки. Пример:

      import com.haulmont.cuba.core.EntityManager
      import com.haulmont.cuba.core.Query
      
      EntityManager em = persistence.getEntityManager()
      Query q = em.createQuery('select count(o) from sales$Order o')
      Number count = q.getSingleResult()
      
      style = count > 0 ? 'emphasized' : null
      return count

      Для отображения указанного скриптом стиля тема приложения должна содержать описание этого стиля для элемента v-tree-node внутри folderspane, например:

      .folderspane .v-tree-node.emphasized {
        font-weight: bold;
      }

В скриптах доступны следующие переменные, установленные в контексте groovy.lang.Binding:

  • folder - экземпляр сущности AppFolder - папка, для которой выполняется скрипт

  • userSession - экземпляр UserSession - текущая пользовательская сессия

  • persistence - реализация интерфейса Persistence

  • metadata - реализация интерфейса Metadata

При обновлении папок для всех скриптов используется один экземпляр groovy.lang.Binding, поэтому между ними можно передавать переменные для исключения дублирующихся запросов и повышения производительности.

Тексты скриптов могут содержаться либо непосредственно в атрибутах сущности AppFolder, либо в отдельных файлах. В последнем случае атрибут должен содержать путь к файлу скрипта (обязательно с расширением ".groovy") по правилам интерфейса Resources . Таким образом, если содержимое атрибута представляет собой строку, заканчивающуюся на ".groovy", текст скрипта загружается из указанного файла, в противном случае в качестве скрипта используется само содержимое атрибута.

Папки приложения представляют собой экземпляры сущности AppFolder и хранятся в связанных таблицах SYS_FOLDER и SYS_APP_FOLDER.

4.7.12.2. Папки поиска

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

Для создания глобальной папки пользователь должен иметь специфическое право Создание/изменение глобальных папок поиска (код cuba.gui.searchFolder.global).

Фильтр папки поиска можно изменить после ее создания - для этого достаточно открыть папку и в экране изменить фильтр Папка: {имя папки}. После сохранения фильтра он будет изменен и в папке тоже.

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

4.7.12.3. Наборы

Использование наборов в экране возможно, если для компонента Filter в атрибуте applyTo указан соответствующий компонент Table . Например:

<layout>
  <filter id="customerFilter"
          datasource="customersDs"
          applyTo="customersTable"/>

  <groupTable id="customersTable"
              width="100%">
      <buttonsPanel>
          <button action="customersTable.create"/>
...
      </buttonsPanel>
...

При этом в контекстном меню таблицы появятся команды Добавить в набор или Добавить в тек. набор / Удалить из набора. Если таблица содержит внутри себя компонент buttonsPanel (как в приведенном выше примере), команды контекстного меню будут продублированы соответствующими кнопками.

Наборы представляют собой экземпляры сущности SearchFolder и хранятся в связанных таблицах SYS_FOLDER и SEC_SEARCH_FOLDER.

4.7.13. Ссылки на экраны

Блок Web Client позволяет открывать экраны приложения по команде, переданной в URL. Причем если в данный момент в браузере нет сессии приложения с зарегистрированным пользователем, то сначала будет отображено окно логина, и сразу после успешной регистрации - главное окно приложения с требуемым экраном.

Набор возможных команд указывается в свойстве приложения cuba.web.linkHandlerActions , по умолчанию это команды open и o. При обработке HTTP запроса анализируется последняя часть URL, и если она совпадает с одной из команд, управление передается бину LinkHandler. Стандартная реализация этого бина позволяет указывать следующие параметры:

  • screen - имя экрана, указанное в screens.xml , например:

    http://localhost:8080/app/open?screen=sec$User.browse
  • item - экземпляр сущности для передачи в экран редактирования, закодированный по правилам класса EntityLoadInfo, т.е. entityName-instanceId или entityName-instanceId-viewName. Для открытия экрана создания нового экземпляра сущности в данном параметре нужно передать строку вида NEW-entityName. Примеры:

    http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93
    
    http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93-user.edit
    
    http://localhost:8080/app/open?screen=sec$User.edit&item=NEW-sec$User
  • params - параметры экрана, передаваемые в метод init() контроллера. Параметры кодируются в виде name1:value1,name2:value2. Значениями параметров могут быть экземпляры сущностей, в свою очередь закодированные по правилам класса EntityLoadInfo. Примеры:

    http://localhost:8080/app/open?screen=sales$Customer.lookup&params=p1:v1,p2:v2
    
    http://localhost:8080/app/open?screen=sales$Customer.lookup&params=p1:sales$Customer-01e37691-1a9b-11de-b900-da881aea47a6

Бин LinkHandler может быть переопределен в проекте приложения для обеспечения специфической обработки ссылок. LinkHandler является прототипом, поэтому обязательно укажите атрибут scope при определении вашего бина в spring.xml, например:

<!-- web-spring.xml -->
<bean id="cuba_LinkHandler" class="com.company.sample.web.MyLinkHandler" scope="prototype"/>

4.7.14. Инспектор сущностей

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

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

Точкой входа в инспектор является экран com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml.

Если в экран передан параметр entity типа String с именем сущности, то инспектор отобразит список экземпляров этой сущности с возможностью фильтрации, выбора и редактирования экземпляров. Параметр может быть указан при регистрации экрана в screens.xml , например:

screens.xml

<screen id="sales$Product.lookup"
      template="/com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml">
  <param name="entity"
         value="sales$Product"/>
</screen>

menu.xml

<item id="sales$Product.lookup"/>

Идентификатор экрана вида {имя_сущности}.lookup дает возможность использовать этот экран компонентам PickerField и LookupPickerField в стандартном действии PickerField.LookupAction.

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

<item id="entityInspector.browse"/>

4.7.15. Информация об используемом ПО

Платформа предоставляет средства для регистрации и отображения в пользовательском интерфейсе информации об используемом в приложении стороннем программном обеспечении (credits). Информация включает в себя название, ссылку на веб-сайт и текст лицензии.

Базовые проекты платформы содержат собственные файлы описаний cuba-credits.xml, reports-credits.xml и т.д. В проекте приложения можно создать аналогичный файл и в свойстве приложения cuba.creditsConfig определить список файлов описаний в зависимости от используемых базовых проектов.

Структура файла credits.xml:

  • Элемент items - перечисление используемых библиотек с указанием текста лицензии либо во вложенном элементе license, либо атрибутом license со ссылкой на текст в секции licenses.

    Cсылаться можно на лицензии, объявленные не только в этом же файле, но и в любом другом файле, объявленном в переменной cuba.creditsConfig раньше, чем текущий.

  • Элемент licenses - перечисление текстов общеупотребительных лицензий.

Для отображения общего списка используемого ПО предназначен фрейм com/haulmont/cuba/gui/app/core/credits/credits-frame.xml, загружающий информацию из файлов, заданных в свойстве cuba.creditsConfig. Пример использования фрейма в экране:

<layout expand="creditsBox">
  <groupBox id="creditsBox"
            caption="msg://credits"
            width="100%">
      <iframe id="credits"
              src="/com/haulmont/cuba/gui/app/core/credits/credits-frame.xml"
              width="100%"
              height="100%"/>
  </groupBox>
</layout>

Если экран с фреймом открывается в модальном режиме (WindowManager.OpenType.DIALOG), ему необходимо задать высоту, иначе возможна неправильная работа скроллинга. Это можно сделать, например, в контроллере экрана, выводящего фрейм:

@Override
public void init(Map<String, Object> params) {
  getDialogParams().setWidth(500).setHeight(400);
}

4.8. Расширение функциональности

Платформа позволяет расширять и переопределять свою функциональность в приложениях в следующих аспектах:

  • расширение набора атрибутов сущностей

  • расширение функциональности экранов

  • расширение и переопределение бизнес-логики, сосредоточенной в бинах Spring

Рассмотрим две первые задачи на примере добавления поля "Адрес" в сущность User подсистемы безопасности платформы.

4.8.1. Расширение сущности

Создадим в проекте приложения класс сущности, унаследованный от com.haulmont.cuba.security.entity.User и добавим в него требуемый атрибут с соответствующими методами доступа:

@Entity(name = "sales$User")
@Extends(User.class)
public class ExtUser extends User {

  @Column(name = "ADDRESS", length = 100)
  private String address;

  public String getAddress() {
      return address;
  }

  public void setAddress(String address) {
      this.address = address;
  }
}

В аннотации @Entity должно быть указано новое имя сущности. Так как базовая сущность не объявляет стратегию наследования, то по умолчанию это SINGLE_TABLE. Это означает, что унаследованная сущность будет храниться в той же таблице, что и базовая, и аннотация @Table не требуется. Другие аннотации базовой сущности - @NamePattern , @Listeners и прочие - автоматически применяются к расширяющей сущности, но могут быть переопределены в ее классе.

Важным элементом класса новой сущности является аннотация @Extends с базовым классом в качестве параметра. Она позволяет сформировать реестр расширяющих сущностей, и заставить механизмы платформы использовать их повсеместно вместо базовых. Реестр реализуется классом ExtendedEntities, который является бином Spring с именем cuba_ExtendedEntities, и доступен также через интерфейс Metadata .

Добавим локализованное название нового атрибута в пакет com.sample.sales.entity:

messages.properties

ExtUser.address=Address

messages_ru.properties

ExtUser.address=Адрес

Зарегистрируем новую сущность в файле persistence.xml проекта:

<class>com.sample.sales.entity.ExtUser</class>

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

alter table SEC_USER add ADDRESS varchar(100)

4.8.2. Расширение экранов

Платформа позволяет создавать новые XML-дескрипторы экранов путем наследования от существующих.

Наследование XML выполняется путем указания в корневом элементе window атрибута extends, содержащего путь к базовому дескриптору.

Правила переопределения элементов XML экрана:

  • Если в расширяющем дескрипторе указан некоторый элемент, в базовом дескрипторе будет произведен поиск соответствующего элемента по следующему алгоритму:

    • Если переопределяющий элемент - view, то ищется соответствующий элемент по атрибутам name, class, entity.

    • Если переопределяющий элемент - property, то ищется соответствующий элемент по атрибуту name.

    • В других случаях, если в переопределяющем элементе указан атрибут id, ищется соответствующий элемент с таким же id.

    • Если поиск дал результат, то найденный элемент переопределяется.

    • Если поиск не дал результата, то определяется, сколько в базовом дескрипторе элементов по данному пути и с данным именем. Если ровно один - он переопределяется.

    • Если поиск не дал результата, и в базовом дескрипторе по данному пути с данным именем нет элементов, либо их больше одного, добавляется новый элемент.

  • В переопределяемом либо добавляемом элементе устанавливается текст из расширяющего элемента.

  • В переопределяемый либо добавляемый элемент копируются все атрибуты из расширяющего элемента. При совпадении имени атрибута значение берется из расширяющего элемента.

  • Добавление нового элемента по умолчанию производится в конец списка соседних элементов. Чтобы добавить новый элемент в начало или с произвольным индексом, необходимо выполнить следующее:

    • определить в расширяющем дескрипторе дополнительный namespace: xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"

    • добавить в расширяющий элемент атрибут ext:index с желаемым индексом, например: ext:index="0".

Для отладки преобразования дескрипторов можно включить вывод в журнал сервера результирующего XML. Делается это путем указания уровня TRACE для логгера com.haulmont.cuba.gui.xml.XmlInheritanceProcessor в файле конфигурации Log4j:

<appender name="FILE" ...
      <param name="Threshold" value="TRACE"/>
...
<category name="com.haulmont.cuba.gui.xml.XmlInheritanceProcessor">
  <priority value="TRACE"/>
</category>

Пример XML-дескриптора экрана браузера сущностей ExtUser:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
      xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
      extends="/com/haulmont/cuba/gui/app/security/user/browse/user-browse.xml">
  <layout>
      <groupTable id="usersTable">
          <columns>
              <column id="address" ext:index="2"/>
          </columns>
      </groupTable>
  </layout>
</window>

В данном примере дескриптор унаследован от стандартного браузера сущностей User платформы, и в таблицу добавлена колонка address с индексом 2, т.е. отображающаяся после login и name.

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

<screen id="sec$User.browse"
      template="com/sample/sales/gui/extuser/extuser-browse.xml"/>
<screen id="sec$User.lookup"
      template="com/sample/sales/gui/extuser/extuser-browse.xml"/>

Аналогично создаем экран редактирования:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
      xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
      extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml">
  <layout>
      <fieldGroup id="fieldGroup">
          <column id="fieldGroupColumn2">
              <field id="address" ext:index="4"/>
          </column>
      </fieldGroup>
  </layout>
</window>

Регистрируем его в screens.xml с идентификатором базового экрана:

<screen id="sec$User.edit"
      template="com/sample/sales/gui/extuser/extuser-edit.xml"/>

После выполнения описанных выше действий в приложении вместо платформенной сущности User будет использоваться ExtUser с соответствующими экранами.

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

4.8.3. Расширение бизнес-логики

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

Для подмены реализации бина достаточно создать свой класс, реализующий интерфейс или расширяющий базовый класс платформы, и зарегистрировать его в spring.xml приложения. Аннотацию @ManagedBean в расширяющем классе применять нельзя, переопределение бинов возможно только с помощью конфигурации в XML.

Рассмотрим пример добавления метода в бин PersistenceTools .

Создаем класс с нужным методом:

public class ExtPersistenceTools extends PersistenceTools {

  public Entity reloadInSeparateTransaction(final Entity entity, final String... viewNames) {
      Entity result = persistence.createTransaction().execute(new Transaction.Callable<Entity>() {
          @Override
          public Entity call(EntityManager em) {
              return em.reload(entity, viewNames);
          }
      });
      return result;
  }
}

Регистрируем класс в spring.xml модуля core проекта с тем же идентификатором, что и бин платформы:

<bean id="cuba_PersistenceTools" class="com.sample.sales.core.ExtPersistenceTools"/>

После этого контекст Spring вместо экземпляра базового класса PersistenceTools будет всегда возвращать ExtPersistenceTools, например:

Persistence persistence;
PersistenceTools tools;

persistence = AppBeans.get(Persistence.class);
tools = persistence.getTools();
assertTrue(tools instanceof ExtPersistenceTools);

tools = AppBeans.get(PersistenceTools.class);
assertTrue(tools instanceof ExtPersistenceTools);

tools = AppBeans.get(PersistenceTools.NAME);
assertTrue(tools instanceof ExtPersistenceTools);

Глава 5. Разработка приложений

Данная глава содержит практическую информацию по созданию приложений на основе платформы.

5.1. Рекомендуемый стиль кода

Форматирование кода

  • Для Java и Groovy кода рекомендуется придерживаться стандартного стиля, описанного в документе Code Conventions for the Java Programming Language. При программировании в IntelliJ IDEA для этого достаточно использовать стиль по умолчанию, а для переформатирования применять сочетание клавиш Ctrl-Alt-L.

    Максимальная длина строки − 120 символов. Длина отступа - 4 символа, использование пробелов вместо символов табуляции включено.

  • XML код: длина отступа - 4 символа, использование пробелов вместо символов табуляции включено.

Соглашения по именованию

ИдентификаторПравило именованияПример
Java и Groovy классы
Класс контроллера экрана

UpperCamelCase

Контроллер экрана списка сущностей − {КлассСущности}Browse

Контроллер экрана редактирования − {КлассСущности}Edit

CustomerBrowse

OrderEdit

XML дескрипторы экранов
Идентификатор компонента, имена параметров в запросах

lowerCamelCase, только буквы и цифры

attributesTable

:component$relevantTo

:ds$attributesDs

Идентификатор источника данных

lowerCamelCase, только буквы и цифры, оканчивается на Ds

attributesDs
SQL скрипты
Зарезервированные словаlowercase create table
ТаблицыUPPER_CASE. Название предваряется именем проекта для формирования пространства имен. В именах таблиц рекомендуется использовать единственное число.

SALES_CUSTOMER

КолонкиUPPER_CASE

CUSTOMER

TOTAL_AMOUNT

Колонки внешних ключейUPPER_CASE. Состоит из имени таблицы, на которую ссылается колонка (без префикса проекта), и суффикса _ID. CUSTOMER_ID
ИндексыUPPER_CASE. Состоит из префикса IDX_, имени таблицы, для которой создается индекс (с префиксом проекта), и имен полей, включенных в индекс. IDX_SALES_CUSTOMER_NAME

5.2. Файловая структура проекта

Рассмотрим файловую структуру проекта на примере простого приложения Sales, состоящего из блоков Middleware, Web Client и Web Portal.

Рисунок 36. Файловая структура проекта

Файловая структура проекта

В корне проекта расположены скрипты сборки build.gradle, settings.gradle и проектные файлы IntelliJ IDEA.

В каталоге modules расположены подкаталоги модулей проекта − global, core, gui, portal, web.

Рисунок 37. Структура модуля global

Структура модуля global

Модуль global содержит каталог исходных текстов src, в корне которого располагаются конфигурационные файлы metadata.xml , persistence.xml и views.xml. Пакет com.sample.sales.service содержит интерфейсы сервисов Middleware, пакет com.sample.sales.entity - классы сущностей и файлы локализации для них.

Рисунок 38. Структура модуля core

Структура модуля core

Модуль core содержит следующие каталоги:

Рисунок 39. Структура модуля gui

Структура модуля gui

Модуль gui содержит каталог исходных текстов src, в корне которого располагается конфигурационный файл screens.xml . Пакет com.sample.sales.gui содержит XML-дескрипторы и контроллеры экранов и файлы локализации для них.

Рисунок 40. Структура модуля web

Структура модуля web

Модуль web содержит следующие каталоги:

5.3. Описание скриптов сборки

Для сборки проектов на основе платформы используется система сборки Gradle. Скрипты сборки представляют собой два файла в корневом каталоге проекта:

  • settings.gradle - задает название и состав модулей проекта

  • build.gradle - определяет конфигурацию сборки.

В данном разделе описывается структура скриптов, а также предназначение и параметры задач (tasks) Gradle.

5.3.1. Структура build.gradle

Секция allprojects задает группу и версию собираемых артефактов проекта. Имена артефактов формируются на основе имен модулей, заданных в settings.gradle. Если свойство ext.isSnapshot равно true, то в именах артефактов будет присутствовать суффикс SNAPSHOT. Свойство ext.tomcatDir задает расположение каталога установки Tomcat. Кроме того, в секции allprojects могут быть заданы следующие опциональные свойства:

  • ext.copyright - текст Copyright Notice, вставляемый IntelliJ IDEA в файлы исходных текстов.

  • ext.vcs - тип используемой в проекте VCS. Если данное свойство указано, то в сгенерированных проектных файлах IntelliJ IDEA будет установлен параметр интеграции с данной VCS. Возможные значения: svn, Git.

  • ext.uploadUrl - URL репозитория, в который будут выгружатся собранные артефакты проекта при выполнении задачи uploadArchives. По умолчанию используется репозиторий Haulmont.

  • ext.uploadUser - имя пользователя репозитория для выгрузки собранных артефактов проекта. По умолчанию используется значение переменной окружения HAULMONT_REPOSITORY_USER.

  • ext.uploadPassword - пароль пользователя репозитория для выгрузки собранных артефактов проекта. По умолчанию используется значение переменной окружения HAULMONT_REPOSITORY_PASSWORD.

  • Свойства ext.tomcatPort, ext.tomcatShutdownPort и ext.tomcatDebugPort оказывают влияние на задачу setupTomcat (см. ниже) и могут быть использованы для установки Tomcat с нестандартными портами.

Любое из свойств проекта можно задать не в самом build.gradle, а путем передачи в командной строке аргумента с префиксом -P, например:

gradlew uploadArchives -PuploadUser=myuser -PuploadPassword=mypassword

В секции buildscript выполняется следующее:

  • Задается версия базовых проектов платформы, на которой основан данный проект.

  • Задается набор репозиториев, из которых будут загружаться зависимости проекта. В качестве имени и пароля для доступа к репозиторию зависимостей используются либо значения свойств проекта repoUser и repoPass, либо стандартные значения, явно указанные в скрипте сборки. Как и другие свойства проекта, repoUser и repoPass можно передать в командной строке в аргументах -P.

  • Объявляется зависимость от плагина cuba-plugin, в котором сосредоточена специфика сборки проектов на платформе. Плагин подключается далее в конфигурацию сборки модулей с помощью метода

    apply(plugin: 'cuba')

Далее в секциях configure определяются параметры сборки модулей приложения.

Исполняемыми единицами в Gradle являются задачи (tasks). Они задаются как внутри плагинов, так и в самом скрипте сборки. Рассмотрим специфические для CUBA задачи, параметры которых могут быть сконфигурированы в build.gradle.

  • enhance - задача типа CubaEnhancing, выполняющая bytecode enhancement классов персистентных сущностей. Объявляется в модуле global. В параметре задачи persistenceXml указывается путь к файлу persistence.xml проекта.

    Например:

    task enhance(type: CubaEnhancing) {
        persistenceXml = "${globalModule.projectDir}/src/persistence.xml"
    }
  • enhanceTransient - задача типа CubaEnhanceTransient, выполняющая bytecode enhancement классов неперсистентных сущностей. В параметре задачи metadataXml указывается путь к файлу metadata.xml проекта.

    Например:

    task enhanceTransient(type: CubaEnhanceTransient) {
        metadataXml = "${globalModule.projectDir}/src/metadata.xml"
    }
  • setupTomcat - задача типа CubaSetupTomcat, выполняющая установку и инициализацию локального сервера Tomcat для последующего быстрого развертывания приложения. Эта задача автоматически добавляется в проект при подключении плагина сборки cuba, поэтому объявлять ее в build.gradle не нужно. Каталог установки Tomcat задается свойством ext.tomcatDir в секции allprojects. По умолчанию это подкаталог build/tomcat проекта.

  • deploy - задача типа CubaDeployment, выполняющая быстрое развертывание модуля в Tomcat. Объявляется в модулях core, web, portal. Параметры:

    • appName - имя веб-приложения, которое будет создано из модуля. Фактически это имя подкаталога внутри tomcat/webapps.

    • jarNames - список имен JAR файлов (без версии), получающихся в результате сборки модуля, которые надо поместить в каталог WEB-INF/lib веб-приложения. Все остальные артефакты модуля и зависимостей будут записаны в tomcat/shared/lib.

    Например:

    task deploy(dependsOn: assemble, type: CubaDeployment) {
        appName = 'app-core'
        jarNames = ['cuba-global', 'cuba-core', 'app-global', 'app-core']
    }
  • buildWar - задача типа CubaWarBuilding, выполняющая сборку модуля в WAR-файл. Может быть объявлена в модулях core, web, portal, если требуется развертывание приложения в WAR. Собранные WAR-файлы находятся в подкаталогах build/distributions модулей.

    Параметры задачи:

    • appName - имя результирующего WAR-файла.

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

      В параметре appHome можно указать как абсолютный путь к домашнему каталогу, так и системную переменную, которая должна быть задана при запуске сервера. Например: appHome = '/work/sales_home' или appHome = '${app.home}'

    • appProperties - мэп свойств, которые будут записаны в файл WEB-INF/local.app.properties в дополнение к определенным в самой задаче. По умолчанию задача buildWar создает данный файл и определяет в нем свойства cuba.logDir, cuba.confDir, cuba.tempDir, cuba.dataDir для работы с домашним каталогом приложения, упомянутым выше. Кроме того, для приложения среднего слоя задается параметр

      cuba.dataSourceJndiName = jdbc/CubaDS

      а для приложения веб-клиента параметры

      cuba.connectionUrlList = http://localhost:8080/${appName}-core
      cuba.useLocalServiceInvocation = false

    Пример задачи в модуле web:

    task buildWar(dependsOn: assemble, type: CubaWarBuilding) {
        appName = 'app'
        appHome = '${app.home}'
        appProperties = ['cuba.connectionUrlList': 'http://server/app-core']
    }

  • createWarDistr - задача типа CubaWarDistribution, выполняющая подготовку дистрибутива, включающего в себя WAR-файлы приложений и их домашний каталог. Задача обязательно должна зависеть от задач buildWar модулей, и имеет следующие параметры:

    • appHome - путь к домашнему каталогу приложения (подробнее см. описание задачи buildWar).

    • distrDir - путь к каталогу, в который копируется содержимое дистрибутива. Это необязательный параметр, по умолчанию используется подкаталог build/war проекта.

    Пример описания задачи:

    task createWarDistr(dependsOn: [coreModule.buildWar, webModule.buildWar], type: CubaWarDistribution) {
        appHome = '${app.home}'
    }
  • createDb - задача типа CubaDbCreation, создающая базу данных приложения путем выполнения соответствующих скриптов. Объявляется в модуле core. Параметры:

    • dbms - тип СУБД, см. Раздел 4.3.1, «Типы СУБД».

    • dbName - имя базы данных.

    • dbUser - имя пользователя СУБД.

    • dbPassword - пароль пользователя СУБД.

    • host - хост и, опционально, порт СУБД в формате host[:port]. Если не задан, используется localhost.

    • masterUrl - URL для подключения при создании БД. Если не задан, используется значение по умолчанию, зависящее от типа СУБД и параметра host.

    • dropDbSql - команда SQL для удаления БД. Если не задана, используется значение по умолчанию, зависящее от типа СУБД.

    • createDbSql - команда SQL для создания БД. Если не задана, используется значение по умолчанию, зависящее от типа СУБД.

    • driverClasspath - список JAR файлов, содержащих JDBC драйвер. Элементы списка разделяются символом ":" на Linux и символом ";" на Windows. Если не задан, используются зависимости, входящие в конфигурацию jdbc данного модуля. Явное задание driverClasspath актуально при использовании Oracle, т.к. его JDBC драйвер не присутствует в зависимостях.

    • oracleSystemPassword - при использовании Oracle пароль пользователя SYSTEM.

    Пример для PostgreSQL:

    task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
        dbms = 'postgres'
        dbName = 'sales'
        dbUser = 'cuba'
        dbPassword = 'cuba'
    }

    Пример для MS SQL Server:

    task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
        dbms = 'mssql'
        dbName = 'sales'
        dbUser = 'sa'
        dbPassword = 'saPass1'
    }

    Пример для Oracle:

    task createDb(dependsOn: assemble, description: 'Creates database', type: CubaDbCreation) {
        dbms = 'oracle'
        host = '192.168.1.10'
        dbName = 'orcl'
        dbUser = 'sales'
        dbPassword = 'sales'
        oracleSystemPassword = 'manager'
        driverClasspath = "$tomcatDir/lib/ojdbc6.jar"
    }
  • updateDb - задача типа CubaDbUpdate, обновляющая базу данных приложения путем выполнения соответствующих скриптов. Аналогична задаче createDb, за исключением отсутствия параметров dropDbSql и createDbSql.

  • startDb - задача типа CubaHsqlStart, выполняющая запуск локального сервера HSQLDB. Параметры:

    • dbName - имя базы данных, по умолчанию cubadb.

    • dbDataDir - каталог, в котором размещена база данных, по умолчанию подкаталог data проекта.

    • dbPort - порт сервера, по умолчанию 9001.

    Например:

    task startDb(type: CubaHsqlStart) {
        dbName = 'sales'
    }

  • stopDb - задача типа CubaHsqlStop, выполняющая остановку локального сервера HSQLDB. Параметры аналогичны задаче startDb.

  • start - задача типа CubaStartTomcat, выполняющая запуск локального сервера Tomcat, установленного задачей setupTomcat. Эта задача автоматически добавляется в проект при подключении плагина cuba, поэтому объявлять ее в build.gradle не нужно.

  • stop - задача типа CubaStopTomcat, выполняющая остановку локального сервера Tomcat, установленного задачей setupTomcat. Эта задача автоматически добавляется в проект при подключении плагина cuba, поэтому объявлять ее в build.gradle не нужно.

  • restart - задача, выполняющая остановку, быстрое развертывание, и старт локального сервера Tomcat.

  • debugWidgetSet - задача типа CubaWidgetSetDebug, которая запускает GWT Code Server для отладки виджетов в веб-браузере.

    Пример использования:

    task debugWidgetSet(type: CubaWidgetSetDebug) {
        widgetSetClass = 'com.haulmont.cuba.web.toolkit.ui.WidgetSet'
    }

    Убедитесь, что кофигурация runtime модуля web-toolkit содержит зависимость от библиотеки Servlet API:

    configure(webToolkitModule) {
        dependencies {
            runtime(servletApi)
        }
    ...

    См. Раздел 5.7.2, «Отладка виджетов в веб-браузере» для получения информации о том как отлаживать код в веб-браузере.

5.3.2. Запуск задач сборки

Задачи (tasks) Gradle, описанные в скриптах сборки, запускаются на исполнение следующими способами:

  • Если работа с проектом ведется с помощью CUBA Studio, то при выполнении пунктов меню Build и Run производится подключение к демону Gradle (запущенному на старте сервера Studio), который и выполняет соответствующие задачи.

  • С помощью исполняемого скрипта gradlew (Gradle wrapper), включенного в проект. Этот скрипт должен находится в корневом каталоге проекта, и может быть создан в Studio с помощью команды Build -> Create Gradle wrapper.

  • С помощью установленного вручную Gradle версии 1.12. В этом случае используется исполняемый скрипт gradle, находящийся в подкаталоге bin установленного Gradle.

Рекомендуется запускать команды gradlew или gradle с ключом --daemon, в этом случае демон Gradle остается в памяти и существенно ускоряет последующее выполнение.

Для удаления демона из памяти используется ключ --stop

Например, чтобы выполнить компиляцию Java файлов и сборку JAR файлов артефактов проекта, необходимо запустить следующую команду:

gradlew --daemon assemble

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

  • idea - создать проектные файлы IntelliJ IDEA. При выполнении этой задачи из репозитория артефактов в локальный кэш Gradle загружаются зависимости вместе со своими исходными кодами.

  • cleanIdea - удалить проектные файлы IntelliJ IDEA.

  • assemble - выполнить компиляцию Java файлов и сборку JAR файлов артефактов проекта в подкаталогах build модулей.

  • clean - удалить подкаталоги build всех модулей проекта.

  • setupTomcat - установить сервер Tomcat в путь, заданный свойством ext.tomcatDir скрипта build.gradle.

  • deploy - быстрое развертывание приложения на сервере Tomcat, предварительно установленном задачей setupTomcat.

  • createDb - создание базы данных приложения и выполнение соответствующих скриптов.

  • updateDb - обновление существующей базы данных приложения путем выполнения соответствующих скриптов.

  • start - запуск сервера Tomcat.

  • stop - остановка запущенного сервера Tomcat.

  • restart - последовательное выполнение задач stop, deploy, start.

5.3.3. Сборка на сервере Continuous Integration

Плагин CUBA для Gradle требует интерактивного принятия условий лицензии CUBA, если это первая сборка для данного пользователя. При автоматической сборке на CI-сервере это невозможно, поэтому существует два пути обхода интерактивного действия:

  1. Создайте файл ${user.home}/.haulmont/license.properties, где ${user.home} - домашний каталог пользователя, от которого работает CI-сервер, и добавьте в него следующее:

    accepted=true
  2. Если создание файла в домашнем каталоге нежелательно, укажите следующий параметр командной строки Gradle:

    -PlicenseAgreementAccepted=true

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

Рекомендуемый способ создания нового проекта - использование CUBA Studio. Пример рассмотрен в главе Быстрый старт данного руководства.

После создания проекта вы можете продолжить разрабатывать его в Studio, либо создать файлы проекта для IntelliJ IDEA или Eclipse и открыть проект в IDE.

5.5. Проектирование БД

В данном разделе приведены практические рекомендации по работе с базой данных на этапе разработки приложения.

Рекомендации по работе с базой данных на стадии эксплуатации приложения приведены в Раздел 6.5, «Создание и обновление БД при эксплуатации приложения».

5.5.1. Создание схемы БД

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

Задача по созданию и поддержке схемы БД состоит из двух частей: создание скриптов и их выполнение.

Скрипты могут быть созданы как вручную, так и с помощью Studio. Рассмотрим процесс создания скриптов в Studio. Для этого выполните команду Generate DB scripts, расположенную в секции Entities. При этом Studio подключается к базе данных, определенной на странице Project properties, и сравнивает имеющуюся схему БД с текущей моделью данных.

Если база данных отсутствует, либо в ней нет таблиц SYS_DB_CHANGELOG и SEC_USER, то генерируются только скрипты инициализации БД. В противном случае создаются также и скрипты обновления. Затем открывается страница, содержащая сгенерированные скрипты.

На вкладке Update scripts отображаются скрипты обновления. Скрипты со статусом new отражают разницу между текущим состоянием модели данных и схемы БД. Для каждой создаваемой или изменяемой таблицы создается отдельный скрипт. В отдельные скрипты помещаются также наборы ограничений целостности таблиц (referential integrity constraints). При закрытии страницы нажатием OK скрипты сохраняются в каталог db/update/{db_type} модуля core.

Со статусом applied отображаются скрипты, уже имеющиеся в проекте и примененные в БД ранее. Они не могут быть отредактированы или удалены.

На вкладке Update scripts могут также отображаться скрипты со статусом to be deleted. Это файлы, имеющиеся в проекте, но не примененные в БД. При закрытии страницы нажатием OK эти файлы будут удалены. Это нормально, если эти скрипты были созданы вами при предыдущей генерации, но не были применены вызовом Update database. В этом случае они больше не нужны, так как текущая разница между схемой БД и моделью данных отражена в новых только что сгенерированных скриптах. Если же, например, эти скрипты получены вами из системы контроля версий от другого разработчика, то вам следует отменить сохранение и сначала применить чужие скрипты на своей БД, а уже потом генерировать новые.

Вкладки Init tables, Init constraints, Init data отображают скрипты создания БД, располагающиеся в каталоге db/init/{db_type} модуля core.

На вкладке Init tables отображается скрипт создания таблиц 10.create-db.sql. Код, относящийся к одной таблице, отделяется комментариями begin {table_name} ... end {table_name}. При изменении некоторой сущности в модели Studio заменит код только между комментариями для соответствующей таблицы, не трогая остальной код, в который могли быть внесены ручные изменения. Поэтому при ручном редактировании не удаляйте эти комментарии, иначе Studio не сможет правильно встраивать свои изменения в существующие файлы.

На вкладке Init constraints отображается скрипт создания ограничений целостности 20.create-db.sql. В нем также присутствуют разделяющие таблицы комментарии, которые нельзя удалять.

На вкладке Init data отображается скрипт 30.create-db.sql, предназначенный для внесения дополнительной информации при инициализации БД. Это могут быть, например, функции, триггеры, или DML операторы для наполнения базы необходимыми данными. Содержимое данного скрипта создается вручную при необходимости.

На начальной стадии разработки приложения, когда модель данных активно меняется, рекомендуется пользоваться только скриптами создания БД (расположенными на вкладках Init tables, Init constraints, Init data), а скрипты обновления на вкладке Update scripts удалять сразу после вызова команды Generate DB scripts. Это наиболее простой и надежный способ поддержания БД в актуальном состоянии. Разумеется, он имеет один существенный недостаток - при применении скриптов БД пересоздается с нуля, поэтому все внесенные в нее данные теряются. Этот недостаток можно частично компенсировать на этапе разработки, добавив в скрипт Init data команды для создания первичных данных при инициализации.

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

Для применения скриптов используйте механизм выполнения скриптов БД задачами Gradle: чтобы пересоздать базу данных полностью, вызовите в главном меню пункт Run -> Create database, а чтобы применить скрипты обновления - пункт Run -> Update database. Следует иметь в виду, что эти пункты доступны, только если сервер приложения остановлен. Разумеется, соответствующие задачи Gradle (createDb и updateDb) можно вызвать в любой момент из командной строки, но если при этом база данных или какие-либо ее объекты заняты, выполнение скриптов завершится с ошибкой.

5.5.2. Подключение к HSQLDB внешними инструментами

HSQLDB, он же HyperSQL, является удобной СУБД для прототипирования приложений, так как не требует установки, и запускается автоматически в CUBA Studio, если для проекта выбрано использование этой СУБД. В данном разделе описаны способы подключения к базе данных HSQLDB внешними инструментами, позволяющими работать со структурой и данными напрямую средствами SQL.

5.5.2.1. Подключение с помощью Squirrel SQL

SQuirreL SQL Client является свободно распространяемым Java-приложением, позволяющим работать с базами данных через JDBC. Загрузить Squirrel SQL можно по адресу http://squirrel-sql.sourceforge.net.

Запустите Squirrel SQL и перейдите на вкладку Drivers. Выделите в списке драйвер HSQLDB Server, нажмите на правую кнопку мыши и выберите Modify Driver.

Рисунок 41.


Перейдите в открывшемся окне на вкладку Extra Class Path и нажмите на кнопку Add, чтобы добавить .jar-файл с драйвером.

Рисунок 42.


Далее нужно выбрать драйвер hsqldb-x.x.x.jar. Можно воспользоваться JAR-файлом, который поставляется вместе с CUBA Studio - он находится в подкаталоге lib.

Рисунок 43.


Далее создайте алиас для подключения к базе данных приложения.

Рисунок 44.


В открывшемся окне укажите параметры подключения - Database URL, пользователя и пароль. По-умолчанию пользователь - sa, пароль отсутствует. Database URL можно найти на вкладке Project properties в CUBA Studio или скопировать из файла modules/core/web/META-INF/context.xml проекта.

Рисунок 45.


5.5.2.2. Подключение с помощью IntelliJ IDEA Ultimate

IntelliJ IDEA Ultimate Edition имеет удобные средства работы с базами данных. Рассмотрим, как подключиться к HSQLDB с ее помощью. Запустите IDEA и откройте панель Database.

Создайте новый источник данных с помощью контекстного меню.

В открывшемся окне выберите драйвер hsqldb-x.x.x.jar. Можно воспользоваться JAR-файлом, который поставляется вместе с CUBA Studio - он находится в подкаталоге lib.

Далее необходимо указать свойства источника данных: Dаtabase URL, пользователя и пароль. Database URL можно найти на вкладке Project properties в CUBA Studio или скопировать из файла modules/core/web/META-INF/context.xml проекта. По-умолчанию пользователь - sa, пароль отсутствует.

Если вы используете PostgreSQL в качестве СУБД и uuid в качестве идентификатора, то при редактировании данных в IDEA может возникнуть ошибка ERROR: operator does not exist: uuid = character varying.

Для решения этой проблемы в настройках источника данных перейдите на вкладку Advanced и присвойте свойству stringtype значение unspecified.

Рисунок 46.


5.5.3. Особенности PostgreSQL

Для создания базы данных PostgreSQL на Ubuntu-подобных операционных системах требуется установка пакета postgresql-contrib, содержащего функцию генерации UUID.

5.5.4. Особенности MS SQL Server

Microsoft SQL Server использует кластерные индексы для таблиц.

По умолчанию кластерный индекс создается по первичному ключу таблицы, однако используемые в CUBA-приложении ключи типа UUID плохо подходят для кластерного индекса. Поэтому необходимо для каждой таблицы правильно выбрать и создать кластерный индекс. Поле для кластерного индекса должно быть небольшим и монотонно возрастающим, поэтому ориентировочные правила следующие:

  • Для большинства таблиц подходит поле CREATE_TS. При этом записи будут физически располагаться в порядке их создания.

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

  • Для небольших (< 100 записей) редко изменяемых таблиц тип кластерного индекса не важен, можно оставить ID.

  • Для таблиц сущностей, унаследованных по стратегии JOINED, в которых нет поля CREATE_TS, нужно создать его искусственно с параметром default current_timestamp.

Пример:

create table SALES_CUSTOMER (
    ID uniqueidentifier not null,
    CREATE_TS datetime,
    ...
    primary key nonclustered (ID)
)^

create clustered index IDX_SALES_CUSTOMER_CREATE_TS on SALES_CUSTOMER (CREATE_TS)^

Пример композитной сущности:

create table SALES_ITEM (
    ID uniqueidentifier not null,
    CREATE_TS datetime,
    ...
    ORDER_ID uniqueidentifier,
    ...
    primary key nonclustered (ID),
    constraint FK_SALES_ITEM_ORDER foreign key (ORDER_ID) references SALES_ORDER(ID)
)^

create clustered index IDX_SALES_ITEM_ORDER on SALES_ITEM (ORDER_ID)^

Пример унаследованной сущности:

create table SALES_DOC (
    CARD_ID uniqueidentifier,
    CREATE_TS datetime default current_timestamp,
    NUMBER varchar(50),
    primary key nonclustered (CARD_ID),
    constraint FK_SALES_DOC_CARD foreign key (CARD_ID) references WF_CARD (ID)
)^

create clustered index IDX_SALES_DOC_CREATE_TS on SALES_DOC (CREATE_TS)^

create index IDX_SALES_DOC_CARD on SALES_DOC (CARD_ID)^

5.5.5. Особенности Oracle Database

В связи с политикой распространения JDBC драйвера Oracle его можно скачать только вручную с сайта http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html. После скачивания скопируйте JAR с драйвером ojdbc6.jar в подкаталог lib Studio и подкаталог lib установленного сервера Tomcat. После этого необходимо остановить Studio, остановить демона Gradle, выполнив в командной строке gradle --stop, а затем снова запустить Studio.

5.6. Логгирование

Для ведения логов в платформе используется фреймворк Apache Log4j версии 1.2.

Для вывода в лог рекомендуется использовать Commons Logging API, получая логгер по имени текущего класса. Пример создания логгера и вывода в него:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class ...

    private Log log = LogFactory.getLog(getClass()); // create logger

    private void someMethod() {
        log.debug("someMethod invoked"); // output message with DEBUG level
    }

Настройка логгирования для блоков Middleware, Web Client и Web Portal производится на уровне сервера приложения - в варианте быстрого развертывания это Tomcat. Блок Desktop Client имеет самостоятельную настройку логгирования.

5.6.1. Настройка логгирования в Tomcat

При выполнении задачи Gradle setupTomcat в каталог проекта устанавливается сервер Tomcat, и производится его дополнительная конфигурация. В частности, в подкаталоге tomcat/bin создаются файлы setenv.bat и setenv.sh, а в подкаталоге tomcat/conf файл log4j.xml.

Файлы setenv.* в переменной CATALINA_OPTS в числе прочего устанавливают параметры загрузки конфигурационного файла log4j.xml.

Файл log4j.xml определяет конфигурацию логгирования. Рассмотрим структуру этого файла.

  • Элементы appender задают "устройства вывода" логов. Основными аппендерами являются FILE и CONSOLE. В параметре Threshold аппендера можно задать порог уровня сообщения. По умолчанию порог для файла - DEBUG, для консоли - INFO. Это означает, что в файл выводятся сообщения с уровнями ERROR, WARN, INFO, DEBUG, а в консоль - с уровнями ERROR, WARN и INFO.

    Для файлового аппендера в параметре File задается путь к файлу лога, а в параметре Append - признак, стирать ли содержимое файла при перезапуске сервера, или добавлять в конец. Настройки по умолчанию задают файл tomcat/logs/app.log и режим стирания при перезапуске. На этапе эксплуатации приложения рекомендуется установить для параметра Append значение true.

    Файловый аппендер по умолчанию реализуется классом org.apache.log4j.DailyRollingFileAppender, который ежедневно в 00:00:00 переименовывает накопившийся за день файл лога в имя с прошедшей датой, и начинает новый лог. Это позволяет избежать создания слишком больших файлов логов.

  • Элементы category задают параметры логгеров, через которые производится посылка сообщений из кода программы. Имена категорий иерархические, то есть например настройки для категории com.company.sample влияют на логгеры com.company.sample.core.CustomerServiceBean, com.company.sample.web.CustomerBrowse, если для них явно не заданы собственные настройки.

    Минимальный уровень указывается в элементе priority. Например, если для категории задан приоритет INFO, то сообщения с уровнями DEBUG и TRACE выводиться не будут. Следует иметь в виду, что на вывод сообщения также влияет порог уровня, заданный в аппендере.

Оперативно изменять уровни для категорий и пороги аппендеров для работающего сервера можно с помощью экрана Администрирование -> Журнал сервера, доступного в веб клиенте. Сделанные настройки логгирования действуют только в текущем сеансе работы сервера и в файл не сохраняются. Этот экран позволяет также просматривать и загружать файлы логов из каталога журналов сервера tomcat/logs.

Платформа автоматически добавляет к сообщениям, выводимым в лог, следующую информацию:

  • [приложение] - имя веб приложения, развернутого в Tomcat, код которого выводит данное сообщение. Эта информация помогает различить сообщения от разных блоков приложения (Middleware, Web Client), так как они выводятся в один файл.

  • [пользователь] - логин пользователя приложения, от имени которого в данный момент работает код, выводящий сообщение. Это позволяет в общем логе отслеживать активность конкретных пользователей. Если код, выводящий сообщение, не связан в момент вывода с пользовательской сессией, информация о пользователе не выводится.

Например, следующее сообщение в логе выведено кодом блока Middleware (app-core), работающим от имени пользователя admin:

2013-12-19 18:48:17,282 DEBUG [com.haulmont.cuba.core.app.DataManagerBean] [app-core] [admin] loadList: metaClass=sec$User, view=com.haulmont.cuba.security.entity.User/user.browse, query=select u from sec$User u, max=100

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

Для десктоп клиента файл log4j.xml должен находиться в каталоге исходников модуля desktop проекта. При сборке приложения он упаковывается в соответствующий JAR файл и доступен в CLASSPATH.

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

  • Создайте в каталоге src модуля desktop новый файл, например, sample-log4j.xml, и скопируйте в него содержимое файла cuba-log4j.xml. Файл cuba-log4j.xml находится внутри одного из JAR-файлов платформы и его легко найти поиском в IDE, если правильно сконфигурированы базовые проекты.

  • Установите путь к файлу лога в параметре File аппендера FILE.

  • Добавьте настройки для категорий логгеров вашего проекта.

  • В классе-наследнике com.haulmont.cuba.desktop.App вашего проекта, например SampleApp, переопределите метод getDefaultLog4jConfig() и верните в нем путь относительно корня CLASSPATH к вашему файлу настроек. Например:

    public class SampleApp extends App {
    ...
        @Override
        protected String getDefaultLog4jConfig() {
            return "sample-log4j.xml";
        }
  • При необходимости можно переопределить местонахождение файла конфигурации на старте приложения с помощью системного свойства log4j.configuration .

5.7. Отладка и тестирование

В данном разделе рассмотрены различные аспекты отладки и тестирования CUBA-приложений.

5.7.1. Подключение отладчика

Запустить сервер Tomcat в режиме отладки можно либо выполнением команды сборки

gradle start

либо запуском командного файла bin/debug.* установленного Tomcat.

После этого сервер будет принимать подключения отладчика на порту 8787. Порт можно изменить в файле bin/setenv.* в переменной JPDA_OPTS.

Для пошаговой отладки в Intellij IDEA необходимо в проекте приложения создать новый элемент Run/Debug Configuration типа Remote, и в его поле Port указать 8787.

5.7.2. Отладка виджетов в веб-браузере

Для отладки виджетов на стороне браузера можно использовать GWT Super Dev Mode.

  1. Настройте задачу debugWidgetSet в build.gradle.

  2. Разверните приложение и запустите Tomcat.

  3. Запустите задачу debugWidgetSet:

    gradlew debugWidgetSet

    GWT Code Server будет перекомпилировать ваш widgetset при изменениях кода виджетов.

  4. Откройте страницу http://localhost:8080/app?debug&superdevmode в браузере Chrome и подождите пока widgetset будет построен первый раз.

  5. Откройте консоль отладки браузера:

  6. После изменения Java-кода в модуле web-toolkit обновляйте страницу в браузере. Widgetset будет инкрементально перестраиваться примерно за 8-10 секунд.

5.7.3. Тестирование

5.7.3.1. Модульные тесты

Модульные тесты (unit tests) можно создавать и выполнять и на уровне Middleware, и на клиентском уровне. Для этого платформа включает в себя фреймворки JUnit и JMockit.

Допустим, имеется следующий контроллер экрана:

public class OrderEditor extends AbstractEditor {

    @Named("itemsTable.add")
    protected AddAction addAction;

    @Override
    public void init(Map<String, Object> params) {
        addAction.setWindowId("sales$Product.lookup");
        addAction.setHandler(new Lookup.Handler() {
            @Override
            public void handleLookup(Collection items) {
                // some code
            }
        });
    }
}

Тогда можно написать следующий тест, проверяющий работу метода init():

public class OrderEditorTest {

    OrderEditor editor;

    @Mocked
    Window.Editor frame;

    @Mocked
    AddAction addAction;

    @Before
    public void setUp() throws Exception {
        editor = new OrderEditor();
        editor.setWrappedFrame(frame);
        editor.addAction = addAction;
    }

    @Test
    public void testInit() {
        editor.init(Collections.<String, Object>emptyMap());
        editor.setItem(new Order());

        new Verifications() {
            {
                addAction.setWindowId("sales$Product.lookup");
                addAction.setHandler(withInstanceOf(Window.Lookup.Handler.class));
            }
        };
    }
}

5.7.3.2. Интеграционные тесты Middleware

На уровне Middleware можно создавать интеграционные тесты, которые выполняются в полнофункциональном контейнере Spring с подключением к базе данных. В тестах такого типа можно выполнять код любого слоя внутри Middleware - от сервисов до ORM.

Для создания интеграционных тестов в модуле core проекта приложения должен быть создан базовый класс - наследник CubaTestCase. В этом классе должны быть переопределены методы инициализации доступа к данным и получения списка файлов конфигурации. Например:

public class SalesTestCase extends CubaTestCase {

    @Override
    protected void initDataSources() throws Exception {
        Class.forName("org.postgresql.Driver");
        TestDataSource ds = new TestDataSource("jdbc:postgresql://localhost/sales_test", "cuba", "cuba");
        TestContext.getInstance().bind("java:comp/env/jdbc/CubaDS", ds);
    }

    @Override
    protected List<String> getTestAppProperties() {
        String[] files = {
                "cuba-app.properties",
                "app.properties",
                "test-app.properties",
        };
        return Arrays.asList(files);
    }
}

В качестве базы данных рекомендуется использовать отдельную тестовую БД, которую можно создавать, например, следующей задачей в build.gradle:

configure(coreModule) {
...
    task createTestDb(dependsOn: assemble, description: 'Creates local Postgres database for tests', type: CubaDbCreation) {
        dbms = 'postgres'
        dbName = 'sales_test'
        dbUser = 'cuba'
        dbPassword = 'cuba'
    }

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

  • persistence - ссылка на интерфейс Persistence

  • metadata - ссылка на интерфейс Metadata

  • deleteRecord() - метод, который удобно использовать в tearDown() для удаления тестовых объектов из БД.

Пример теста, проверяющего чтение сущностей из базы данных:

public class CustomerLoadTest extends SalesTestCase {

    private UUID customerId;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        persistence.createTransaction().execute(new Transaction.Runnable() {
            @Override
            public void run(EntityManager em) {
                Customer customer = new Customer();
                customerId = customer.getId();
                customer.setName("testCustomer");
                em.persist(customer);
            }
        });
    }

    @Override
    public void tearDown() throws Exception {
        deleteRecord("SALES_CUSTOMER", customerId);
        super.tearDown();
    }

    public void test() {
        Transaction tx = persistence.createTransaction();
        try {
            EntityManager em = persistence.getEntityManager();
            TypedQuery<Customer> query = em.createQuery(
                "select c from sales$Customer c", Customer.class);
            List<Customer> list = query.getResultList();
            tx.commit();
            assertTrue(list.size() > 0);
        } finally {
            tx.end();
        }
    }
}

5.7.3.3. Интеграционные тесты клиентского уровня

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

Класс клиентского интеграционного теста должен быть унаследован от CubaClientTestCase. В методе @Before необходимо вызвать унаследованные методы addEntityPackage(), setViewConfig() и затем setupInfrastructure() для создания объектов Metadata и Configuration и развертывания метаданных по выбранным сущностям. Далее в методе @Before можно дополнить инфраструктуру необходимыми мок-объектами с помощью конструкции Expectations или NonStrictExpectations.

Пример инициализирующего метода @Before одного из тестов платформы:

@Before
public void setUp() throws Exception {
    addEntityPackage("com.haulmont.cuba.security.entity");
    addEntityPackage("com.haulmont.cuba.core.entity");
    addEntityPackage("com.haulmont.cuba.gui.data.impl.testmodel1");
    setViewConfig("/com/haulmont/cuba/gui/data/impl/testmodel1/test-views.xml");
    setupInfrastructure();

    metadataSession = metadata.getSession();
    dataSupplier = new TestDataSupplier();

    dataSupplier.commitCount = 0;

    new NonStrictExpectations() {
        @Mocked ClientConfig clientConfig;
        @Mocked PersistenceHelper persistenceHelper;
        {
            configuration.getConfig(ClientConfig.class); result = clientConfig;

            clientConfig.getCollectionDatasourceDbSortEnabled(); result = true;

            persistenceManager.getMaxFetchUI(anyString); result = 10000;

            PersistenceHelper.isNew(any); result = false;
        }
    };
}

5.8. Рецепты разработки

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

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

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

  • В XML-дескрипторах экранов атрибуты компонентов, отображающие статичный текст (например caption), могут обращаться к локализованным сообщениям по правилам метода MessageTools.loadString(). Например:

    • caption="msg://roleName" - получить сообщение, заданное ключом roleName в пакете сообщений текущего экрана. Пакет сообщений экрана задается в атрибуте messagesPack корневого элемента window.

    • caption="msg://com.company.sample.entity/Role.name" - получить сообщение, заданное ключом Role.name в пакете сообщений com.company.sample.entity.

  • В контроллерах экранов локализованные сообщения можно получать следующими способами:

    • Из пакета сообщений текущего экрана:

      • Методом getMessage(), унаследованным от базового класса AbstractFrame. Например:

        String msg = getMessage("warningMessage");
      • Методом formatMessage(), унаследованным от базового класса AbstractFrame. В этом случае сообщение используется для форматирования переданных параметров по правилам метода String.format(). Например:

        messages.properties:

        warningMessage = Invalid email address: '%s'

        Java-контроллер:

        String msg = formatMessage("warningMessage", email);
    • Из произвольного пакета сообщений путем инжекции интерфейса инфраструктуры Messages. Например:

      @Inject
      private Messages messages;
      
      @Override
      public void init(Map<String, Object> params) {
          String msg = messages.getMessage(getClass(), "warningMessage");
          ...
      } 
  • В компонентах, управляемых контейнером Spring (управляемых бинах, сервисах, JMX-бинах, контроллерах Spring MVC модуля portal) локализованные сообщения можно получать путем инжекции интерфейса инфраструктуры Messages:

    @Inject
    protected Messages messages;
    ...
    String msg = messages.getMessage(getClass(), "warningMessage");
  • В любом коде приложения, где невозможна инжекция, интерфейс Messages может быть получен с помощью статического метода get() класса AppBeans:

    protected Messages messages = AppBeans.get(Messages.class);
    ...
    String msg = messages.getMessage(getClass(), "warningMessage");

5.8.2. Присвоение начальных значений

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

5.8.2.1. Инициализация полей сущности

Атрибуты простых типов (Boolean, Integer и т.д.) можно инициализировать прямо в объявлении соответствующего поля класса сущности, например:

public class User extends StandardEntity {
...
    @Column(name = "ACTIVE")
    protected Boolean active = true;
...
}

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

public class MyEntity extends StandardEntity {
...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    protected User creator;
...
    @PostConstruct
    protected void init() {
       setCreator(AppBeans.get(UserSessionSource.class).getUserSession().getUser());
    }
}

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

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

Рассмотрим для примера две связанные сущности:

Фрагмент XML-дескриптора экрана, отображающего одновременно списки обоих сущностей:

<dsContext>
    <collectionDatasource id="typesDs"
                          class="com.haulmont.sample.entity.DeviceType"
                          view="_local">
        <query>
            select e from sample$DeviceType e
        </query>
    </collectionDatasource>
    <collectionDatasource id="descriptionsDs"
                          class="com.haulmont.sample.entity.DeviceDescription"
                          view="_local">
        <query>
            select e from sample$DeviceDescription e where e.deviceType.id = :ds$typesDs
        </query>
    </collectionDatasource>
</dsContext>
<layout>
...
        <table id="typeTable">
            <actions>
                <action id="create"/>
                <action id="edit"/>
                <action id="remove"/>
            </actions>
            <columns>
                <column id="name"/>
            </columns>
            <rows datasource="typesDs"/>
        </table>
...
        <table id="descriptionTable">
            <actions>
                <action id="create"/>
                <action id="edit"/>
                <action id="remove"/>
            </actions>
            <columns>
                <column id="description"/>
            </columns>
            <rows datasource="descriptionsDs"/>
        </table>
    </split>
</layout>

Контроллер этого экрана:

public class DeviceTypeBrowse extends AbstractLookup {

    @Inject
    private CollectionDatasource<DeviceType, UUID> typesDs;

    @Named("descriptionTable.create")
    private CreateAction descrCreateAction;

    @Override
    public void init(Map<String, Object> params) {
        typesDs.addListener(new CollectionDsListenerAdapter<DeviceType>() {
            @Override
            public void itemChanged(Datasource<DeviceType> ds, @Nullable DeviceType prevItem, @Nullable DeviceType item) {
                descrCreateAction.setInitialValues(Collections.<String, Object>singletonMap("deviceType", item));
            }
        });
    }
}

В контроллере источнику данных typesDs добавляется слушатель на событие изменения выбранной записи. При смене выбранной записи вызывается метод setInitialValues() действия, и ему передается мэп с одним элементом, ключом которого является навание атрибута - deviceType, а значением - выбранный экземпляр DeviceType. Таким образом при выполнении действия CreateAction в атрибуте deviceType нового экземпляра DeviceDescription будет сразу установлен выбранный в таблице экземпляр DeviceType.

5.8.2.3. Использование метода initNewItem

Начальные значения можно также задать в контроллере экрана создаваемой сущности в методе initNewItem().

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

В XML-дескрипторе экрана редактирования сотрудника объявляем источник данных для экземпляра сотрудника и вложенный источник - для связанного пользователя:

<dsContext>
    <datasource id="employeeDs"
                class="com.haulmont.sample.entity.Employee"
                view="employee-edit">
        <datasource id="userDs"
                    property="user"/>
    </datasource>
</dsContext>

В контроллере экрана редактирования сотрудника определяем:

@Inject
private Metadata metadata;

private Group defaultGroup;
private Role defaultRole;

@Override
protected void initNewItem(Employee item) {
    User user = metadata.create(User.class);
    user.setGroup(defaultGroup);
    final UserRole userRole = metadata.create(UserRole.class);
    userRole.setUser(user);
    userRole.setRole(defaultRole);
    getDsContext().addListener(new DsContext.CommitListenerAdapter() {
        @Override
        public void beforeCommit(CommitContext context) {
            context.getCommitInstances().add(userRole);
        }
    });
    item.setUser(user);
}

Здесь в методе initNewItem() создается новый экземпляр User, и для него устанавливается некоторая группа доступа defaultGroup. Связь с ролью defaultRole устанавливается с помощью нового экземпляра сущности UserRole. Для сохранения этой связи в БД при коммите экрана, экземпляр UserRole добавляется в коллекцию сохраняемых сущностей в методе beforeCommit() слушателя DsContext.CommitListener.

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

5.8.3. Редактирование композитных сущностей

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

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

5.8.3.1. Реализация композиции

Рассмотрим реализацию композиции на примере сущностей Airport и Terminal:

  1. Сущность Terminal содержит обязательную ссылку на Airport:

    @Entity(name = "sample$Terminal")
    @Table(name = "SAMPLE_TERMINAL")
    public class Terminal extends StandardEntity {
    ...    
        @ManyToOne(optional = false, fetch = FetchType.LAZY)
        @JoinColumn(name = "AIRPORT_ID")
        private Airport airport;
    
        public Airport getAirport() {
            return airport;
        }
    
        public void setAirport(Airport airport) {
            this.airport = airport;
        }
    }
  2. Сущность Airport содержит one-to-many коллекцию терминалов. Соответствующее поле помечается аннотацией @Composition для огранизации композиции и @OnDelete для каскадного мягкого удаления:

    @Entity(name = "sample$Airport")
    @Table(name = "SAMPLE_AIRPORT")
    public class Airport extends StandardEntity {
    ...
        @OneToMany(fetch = FetchType.LAZY, mappedBy = "airport")
        @OnDelete(DeletePolicy.CASCADE)
        @Composition
        protected List<Terminal> terminals;
    
        public List<Terminal> getTerminals() {
            return terminals;
        }
    
        public void setTerminals(List<Terminal> terminals) {
            this.terminals = terminals;
        }
    }
  3. Представление, используемое в экране редактирования аэропорта, должно содержать атрибут-коллецию terminals:

    <view entity="sample$Airport" name="airport-edit" extends="_local">
        <property name="terminals" view="_local"/>
    </view>

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

  4. В XML-дескрипторе экрана редактирования аэропорта определяем источники данных для экземпляра Airport и коллекции его терминалов:

    <dsContext>
        <datasource id="airportDs"
                    class="com.haulmont.sample.entity.Airport"
                    view="airport-edit">
            <collectionDatasource id="terminalsDs" property="terminals"/>
        </datasource>
    </dsContext>
  5. В XML-дескрипторе экрана редактирования аэропорта определяем таблицу, отображающую терминалы, и стандартные действия для нее:

    <table id="terminalsTable">
        <actions>
            <action id="create"/>
            <action id="edit"/>
            <action id="remove"/>
        </actions>
        <buttonsPanel>
            <button action="terminalsTable.create"/>
            <button action="terminalsTable.edit"/>
            <button action="terminalsTable.remove"/>
        </buttonsPanel>
        <columns>
            <column id="code"/>
            <column id="name"/>
            <column id="address"/>
        </columns>
        <rows datasource="terminalsDs"/>
    </table>
  6. В экране редактирования терминала достаточно определить стандартные элементы: datasource для экземпляра Terminal и визуальные компоненты, связанные с этим datasource, для редактирования атрибутов терминала.

В результате редактирование экземпляра аэропорта работает следующим образом:

  • В экране редактирования аэропорта отображается таблица терминалов.

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

  • Пользователь может создавать новые или удалять терминалы - все изменения сохраняются в источнике данных terminalsDs.

  • Пользователь нажимает OK в экране редактирования аэропорта, и измененный Airport вместе со всеми измененными экземплярами Terminal отправляется на Middleware в метод DataManager.commit() и сохраняется в базе данных в рамках одной транзакции.

5.8.3.2. Глубокая композиция

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

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

  1. Атрибуту meetingPoints класса Terminal добавить аннотации @Composition и @OnDelete аналогично атрибуту terminals класса Airport.

  2. Создать новое представление для Terminal:

    <view entity="sample$Terminal" name="terminal-edit" extends="_local">
        <property name="meetingPoints" view="_local"/>
    </view>

    И использовать его в представлении Airport вместо _local:

    <view entity="sample$Airport" name="airport-edit" extends="_local">
        <property name="terminals" view="terminal-edit"/>
    </view>
  3. В XML-дескрипторе экрана редактирования аэропорта определить источники данных для экземпляра Airport и вложенных сущностей на всю глубину композиции:

    <dsContext>
        <datasource id="airportDs"
                    class="com.haulmont.sample.entity.Airport"
                    view="airport-edit">
            <collectionDatasource id="terminalsDs" property="terminals">
                <collectionDatasource id="meetingPointsDs" property="meetingPoints"/>
            </collectionDatasource>  
        </datasource>
    </dsContext>

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

  4. В XML-дескрипторе экрана редактирования терминала в свою очередь определить для коллекции meetingPoints вложенный источник данных и соответствующую таблицу.

В результате измененные эземпляры MeetingPoint, так же как и экземпляры Terminal, будут сохраняться в базу данных только вместе с экземпляром Airport в одной транзакции.

5.8.4. Выполнение кода на старте приложения

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

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

Если атрибут name сущности User изменяется, например через стандартный экран управления пользователями, необходимо, чтобы изменялся также и атрибут name связанной сущности Employee. Это обычная задача для "денормализованных" данных, и решается она, как правило, с использованием entity listeners. В данном случае ситуация осложняется тем, что необходимо отслеживать изменения не проектной, а платформенной сущности User, и добавить entity listener с помощью аннотации @Listeners невозможно. Однако, можно добавить listener динамически через бин EntityListenerManager, и сделать это лучше всего на старте приложения.

Для этого создадим в модуле core приложения бин AppLifecycle, имплементирующий интерфейс AppContext.Listener, и зарегистрируем его вызовом метода AppContext.addListener() в конструкторе объекта:

@ManagedBean("sample_AppLifecycle")
public class AppLifecycle implements AppContext.Listener {

    @Inject
    private EntityListenerManager entityListenerManager;

    public AppLifecycle() {
        AppContext.addListener(this);
    }

    @Override
    public void applicationStarted() {
        entityListenerManager.addListener(User.class, UserEntityListener.class);
    }

    @Override
    public void applicationStopped() {
    }

    public static class UserEntityListener implements BeforeUpdateEntityListener<User> {
        @Override
        public void onBeforeUpdate(User user) {
            Persistence persistence = AppBeans.get(Persistence.class);
            if (persistence.getTools().getDirtyFields(user).contains("name")) {
                EntityManager em = persistence.getEntityManager();
                TypedQuery<Employee> query = em.createQuery(
                        "select e from sample$Employee e where e.user.id = ?1", Employee.class);
                query.setParameter(1, user.getId());
                Employee employee = query.getFirstResult();
                if (employee != null) {
                    employee.setName(user.getName());
                }
            }
        }
    }
}

В результате сразу после старта блока Middleware будет вызван метод applicationStarted() данного бина. В этом методе в качестве entity listener сущности User регистрируется внутренний класс UserEntityListener.

Метод onBeforeUpdate() класс UserEntityListener будет вызываться перед каждым сохранением изменений экземпляров User в базу данных. В методе проверяется, есть ли атрибут name среди измененных, и если да, загружается связанный экземпляр Employee, и в нем устанавливается это же значение name.

5.8.5. Загрузка и вывод изображений

Рассмотрим задачу загрузки, хранения и отображения фотографий сотрудников:

  • Сотрудник представлен сущностью Employee.

  • Файлы изображений хранятся в FileStorage. Сущность Employee содержит ссылку на соответствующий FileDescriptor.

  • Экран редактирования Employee отображает фотографию, а также дает возможность загрузить, выгрузить и очистить изображение.

Класс сущности со ссылкой на файл изображения:

@Table(name = "SAMPLE_EMPLOYEE")
@Entity(name = "sample$Employee")
public class Employee extends StandardEntity {
...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "IMAGE_FILE_ID")
    protected FileDescriptor imageFile;

    public void setImageFile(FileDescriptor imageFile) {
        this.imageFile = imageFile;
    }

    public FileDescriptor getImageFile() {
        return imageFile;
    }
}

Фрагмент XML-дескриптора экрана редактирования Employee:

<groupBox caption="Photo" spacing="true"
          height="250px" width="250px" expand="embeddedImage">
        <embedded id="embeddedImage" width="100%"
                  align="MIDDLE_CENTER"/>
    <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>

Компоненты отображения и загрузки/выгрузки фотографии заключены внутрь контейнера groupBox. В верхней его части с помощью компонента embedded выводится изображение, а в нижней слева направо расположены компонент upload для загрузки файла и кнопки выгрузки и очистки изображения. В результате эта часть экрана должна выглядеть следующим образом:

Теперь рассмотрим контроллер экрана редактирования.

public class EmployeeEdit extends AbstractEditor<Employee> {

    private Log log = LogFactory.getLog(EmployeeEdit.class);

    @Inject
    private DataSupplier dataSupplier;
    @Inject
    private FileStorageService fileStorageService;
    @Inject
    private FileUploadingAPI fileUploading;
    @Inject
    private ExportDisplay exportDisplay;

    @Inject
    private Embedded embeddedImage;
    @Inject
    private FileUploadField uploadField;
    @Inject
    private Button downloadImageBtn;
    @Inject
    private Button clearImageBtn;
    @Inject
    private Datasource<Employee> employeeDs;

    private static final int IMG_HEIGHT = 190;
    private static final int IMG_WIDTH = 220;

    @Override
    public void init(Map<String, Object> params) {
        uploadField.addListener(new FileUploadField.ListenerAdapter() {
            @Override
            public void uploadSucceeded(Event event) {
                FileDescriptor fd = uploadField.getFileDescriptor();
                try {
                    fileUploading.putFileIntoStorage(uploadField.getFileId(), fd);
                } catch (FileStorageException e) {
                    throw new RuntimeException(e);
                }
                getItem().setImageFile(dataSupplier.commit(fd, null));
                displayImage();
            }

            @Override
            public void uploadFailed(Event event) {
                showNotification("Upload failed", NotificationType.HUMANIZED);
            }
        });

        employeeDs.addListener(new DsListenerAdapter<Employee>() {
            @Override
            public void valueChanged(Employee source, String property, 
                                     @Nullable Object prevValue, @Nullable Object value) {
                if ("imageFile".equals(property)) {
                    updateImageButtons(value != null);
                }
            }
        });
    }

    @Override
    protected void postInit() {
        displayImage();
        updateImageButtons(getItem().getImageFile() != null);
    }

    public void onDownloadImageBtnClick(Component source) {
        if (getItem().getImageFile() != null)
            exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
    }

    public void onClearImageBtnClick(Component source) {
        getItem().setImageFile(null);
        displayImage();
    }

    private void updateImageButtons(boolean enable) {
        downloadImageBtn.setEnabled(enable);
        clearImageBtn.setEnabled(enable);
    }

    private void displayImage() {
        byte[] bytes = null;
        if (getItem().getImageFile() != null) {
            try {
                bytes = fileStorageService.loadFile(getItem().getImageFile());
            } catch (FileStorageException e) {
                log.error("Unable to load image file", e);
                showNotification("Unable to load image file", NotificationType.HUMANIZED);
            }
        }
        if (bytes != null) {
            embeddedImage.setSource(getItem().getImageFile().getName(), new ByteArrayInputStream(bytes));
            embeddedImage.setType(Embedded.Type.IMAGE);
            BufferedImage image;
            try {
                image = ImageIO.read(new ByteArrayInputStream(bytes));
                int width = image.getWidth();
                int height = image.getHeight();

                if (((double) height / (double) width) > ((double) IMG_HEIGHT / (double) IMG_WIDTH)) {
                    embeddedImage.setHeight(String.valueOf(IMG_HEIGHT));
                    embeddedImage.setWidth(String.valueOf(width * IMG_HEIGHT / height));
                } else {
                    embeddedImage.setWidth(String.valueOf(IMG_WIDTH));
                    embeddedImage.setHeight(String.valueOf(height * IMG_WIDTH / width));
                }
            } catch (IOException e) {
                log.error("Unable to resize image", e);
            }
            // refresh image
            embeddedImage.setVisible(false);
            embeddedImage.setVisible(true);
        } else {
            embeddedImage.setVisible(false);
        }
    }
}
  • В методе init() сначала инициализируется компонент uploadField, предназначенный для загрузки новой фотографии. В случае успешной загрузки из компонента получается экземпляр нового FileDescriptor, и соответствующий файл отправляется из временного хранилища в постоянное вызовом FileUploadingAPI.putFileIntoStorage(). После этого FileDescriptor сохраняется в БД вызовом DataSupplier.commit(), и сохраненный экземпляр устанавливается в атрибуте imageFile редактируемой сущности Employee. Затем вызывается метод displayImage() контроллера для отображения загруженной фотографии.

    Далее в методе init() источнику данных, содержащему редактируемый экземпляр Employee, добавляется слушатель для запрещения или разрешения кнопок выгрузки и очистки файла в зависимости от того, загружен файл или нет.

  • Метод postInit() вызывает отображение файла и обновляет состояние кнопок в зависимости от наличия загруженного файла.

  • Метод onDownloadImageBtnClick() вызывается при нажатии кнопки downloadImageBtn и выполняет выгрузку файла с помощью интерфейса ExportDisplay.

  • Метод onClearImageBtnClick() вызывается при нажатии кнопки clearImageBtn и очищает атрибут imageFile сущности Employee. Удаления файла из хранилища не производится.

  • Метод displayImage() выгружает файл из хранилища в байтовый массив, устанавливает содержимое компонента embeddedImage, и перерасчитывает его размеры для сохранения пропорций изображения.

    Следует иметь в виду, что выгрузка файлов из хранилища в байтовый массив приемлема только для небольших файлов. Если размер файла непредсказуем, следует использовать только выгрузку через ExportDisplay, при которой файл передается через потоки ввода-вывода и нигде не оказывается в памяти целиком.

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

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

5.8.6.1. Пример использования стороннего компонента Vaadin

Способ подключения сторонних компонентов Vaadin описан в Раздел 4.5.10.1, «Использование сторонних компонентов Vaadin».

Рассмотрим пример использования компонента Stepper, доступного по адресу http://vaadin.com/addon/stepper. Данный компонент позволяет пошагово изменять значение текстового поля с помощью клавиатуры, колесика мыши и встроенных кнопок вверх/вниз.

  • Предположим, что в проекте имеется сущность Customer со строковым атрибутом name. В модуле web создан экран редактирования customer-edit.xml со следующей компоновкой:

    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="customerDs">
            <column width="250px">
                <field id="name"/>
            </column>
        </fieldGroup>
        <iframe id="windowActions"
                screen="editWindowActions"/>
    </layout>

    Нам необходимо добавить атрибут score типа Integer, и обеспечить его пошаговое редактирование в данном экране.

  • В CUBA Studio добавляем атрибут score сущности Customer:

    @Column(name = "SCORE")
    protected Integer score;
    
    public void setScore(Integer score) {
        this.score = score;
    }
    public Integer getScore() {
        return score;
    }

    Генерируем скрипты обновления БД и запускаем обновление.

  • Выполняем команду Create web toolkit module секции Project properties навигатора Studio.

  • В build.gradle проекта добавляем зависимость модуля web от add-on, содержащего компонент:

    configure(webModule) {
        ...
        dependencies {
            ...
            compile("org.vaadin.addons:stepper:2.1.2")
        }
  • Пересоздаем проектные файлы IDE: меню Create or update IDEA project files.

  • В файл AppWidgetSet.gwt.xml модуля web-toolkit проекта подключаем набор виджетов add-on после набора виджетов платформы:

    <module>
        <inherits name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" />
        
        <inherits name="org.vaadin.risto.stepper.widgetset.StepperWidgetset" />
    
        <set-property name="user.agent" value="safari" />

    Для более быстрой сборки виджетов на время разработки можно установить свойство user.agent. В данном примере набор виджетов будет собираться только для браузеров, основанных на WebKit: Chrome, Safari, и т.д.

  • В XML-дескрипторе экрана редактирования customer-edit.xml добавляем кастомное поле score в компонент fieldGroup:

    <fieldGroup id="fieldGroup"
                datasource="customerDs">
        <column width="250px">
            <field id="name"/>
            <field id="score" custom="true" caption="Score"/>
        </column>
    </fieldGroup>
  • В контроллере экрана редактирования CustomerEdit добавляем следующий код:

    public class CustomerEdit extends AbstractEditor<Customer> {
    
        @Inject
        private ComponentsFactory componentsFactory;
        @Inject
        private FieldGroup fieldGroup;
    
        private IntStepper stepper = new IntStepper();
    
        @Override
        public void init(Map<String, Object> params) {
            fieldGroup.addCustomField("score", new FieldGroup.CustomFieldGenerator() {
                @Override
                public Component generateField(final Datasource datasource, final String propertyId) {
                    Component box = componentsFactory.createComponent(BoxLayout.VBOX);
                    com.vaadin.ui.Layout layout = WebComponentsHelper.unwrap(box);
                    layout.addComponent(stepper);
                    stepper.setSizeFull();
    
                    stepper.addValueChangeListener(new Property.ValueChangeListener() {
                        @Override
                        public void valueChange(Property.ValueChangeEvent event) {
                            datasource.getItem().setValue(propertyId, event.getProperty().getValue());
                        }
                    });
    
                    return box;
                }
            });
        }
    
        @Override
        protected void postInit() {
            stepper.setValue(getItem().getScore());
        }
    }

    Здесь в поле stepper создается экземпляр компонента, подключенного из add-on. В методе init() производится инициализация кастомного поля score. Через ComponentsFactory создается экземпляр BoxLayout, затем из него с помощью WebComponentsHelper извлекается ссылка на Vaadin-контейнер, и в этот контейнер добавляется наш новый компонент. BoxLayout возвращается для отображения в кастомном поле.

    Для связи компонента с данными во-первых, в методе postInit() ему устанавливается текущее значение из редактируемого Customer, а во-вторых, добавляется слушатель на изменение значения, который обновляет соответствующий атрибут сущности при изменении значения пользователем.

  • Новый компонент можно использовать и вне FieldGroup в произвольном месте экрана. Для этого в XML-дескрипторе объявим контейнер:

    <hbox id="scoreBox"
          spacing="true">
        <label value="Score"/>
    </hbox>

    В контроллере инжектируем контейнер, извлекаем ссылку на Vaadin-контейнер и добавляем в него компонент:

    public class CustomerEdit extends AbstractEditor<Customer> {
    
        @Inject
        private BoxLayout scoreBox;
    
        private IntStepper stepper = new IntStepper();
    
        @Override
        public void init(Map<String, Object> params) {
            com.vaadin.ui.Layout box = WebComponentsHelper.unwrap(scoreBox);
            box.addComponent(stepper);
    
            stepper.addValueChangeListener(new Property.ValueChangeListener() {
                @Override
                public void valueChange(Property.ValueChangeEvent event) {
                    getItem().setValue("score", event.getProperty().getValue());
                }
            });
        }
    
        @Override
        protected void postInit() {
            stepper.setValue(getItem().getScore());
        }
    }

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

  • Для адаптации внешнего вида компонента создадим в проекте расширение темы. Для этого в Studio выполним команду Create theme extension секции Project properties навигатора. Затем откроем файл themes/havana/havana-ext.scss модуля web, и добавим в него следующий код:

    @import "../havana/havana"; 
     
    @mixin havana-ext { 
      @include havana; 
     
      /* Basic styles for stepper */
      .v-stepper { 
        /* Use box-sizing: border-box; for all browsers */
        @include box-defaults; 
     
        height: 25px; 
        border: 0; 
     
        /* Use theme fonts */
        font-family: $theme_fonts; 
      } 
     
      /* Basic styles for inner text box */
      .v-stepper input[type="text"] { 
        /* Use box-sizing: border-box; for all browsers */
        @include box-defaults; 
     
        height: 25px; 
        padding: 1px; 
        outline: 0; 
        margin: 0; 
     
        /* Use border color from theme */
        border: 1px solid $theme_fieldBorderColor; 
      } 
     
      /* Focused styles */
      .v-stepper.v-stepper input[type="text"]:focus { 
        /* Use focused border color from theme */
        border-color: $theme_fieldFocusedBorderColor; 
        /* hide default focus outline */
        outline: 0; 
      } 
     
      /* Readonly styles */
      .v-readonly.v-stepper input[type="text"], 
      .v-readonly.v-stepper input[type="text"]:focus { 
        /* Use readonly border color from theme */
        border-color: $theme_fieldReadonlyBorderColor; 
      } 
    }

5.8.6.2. Пример интеграции компонента Vaadin в Generic UI

В Раздел 4.5.10.2, «Интеграция компонентов в Generic UI» были рассмотрены принципы интеграции "нативных" компонентов в универсальный UI для того, чтобы их можно было объявлять в XML-дескрипторах экранов и связывать с данными.

В предыдущем разделе мы подключили в проект сторонний компонент Stepper. Рассмотрим процесс интеграции в универсальный UI класса IntStepper, реализующего пошаговое изменение числового значения типа int.

  • Интерфейс компонента в модуле gui:

    package com.company.myproject.gui.components;
    
    import com.haulmont.cuba.gui.components.Field;
    
    public interface IntStepper extends Field {
    
        String NAME = "intStepper";
    
        boolean isManualInputAllowed();
        void setManualInputAllowed(boolean value);
    
        boolean isMouseWheelEnabled();
        void setMouseWheelEnabled(boolean value);
    
        int getStepAmount();
        void setStepAmount(int amount);
    
        int getMaxValue();
        void setMaxValue(int maxValue);
    
        int getMinValue();
        void setMinValue(int minValue);
    }

    В качестве базового для нашего компонента выбран интерфейс Field. Это позволяет осуществить связь с данными (data binding), то есть отображать и редактировать значение некоторого атрибута сущности.

  • Реализация компонента в модуле web:

    package com.company.myproject.web.components;
    
    import com.company.myproject.gui.components.IntStepper;
    import com.haulmont.cuba.web.gui.components.WebAbstractField;
    
    public class WebIntStepper 
            extends WebAbstractField<org.vaadin.risto.stepper.IntStepper> 
            implements IntStepper {
    
        public WebIntStepper() {
            component = new org.vaadin.risto.stepper.IntStepper();
        }
    
        @Override
        public boolean isManualInputAllowed() {
            return component.isManualInputAllowed();
        }
        @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();
        }
        @Override
        public void setMinValue(int minValue) {
            component.setMinValue(minValue);
        }
    }

    В квчестве базового класса выбран WebAbstractField, который реализует логику интерфейса Field по связыванию с источником данных и другие его методы.

  • XML-загрузчик компонента в модуле gui:

    package com.company.myproject.gui.loaders;
    
    import com.company.myproject.gui.components.IntStepper;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.xml.layout.*;
    import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;
    import org.dom4j.Element;
    
    public class IntStepperLoader extends AbstractFieldLoader {
    
        public IntStepperLoader(Context context, LayoutLoaderConfig config, ComponentsFactory factory) {
            super(context, config, factory);
        }
    
        @Override
        public Component loadComponent(ComponentsFactory factory, Element element, Component parent) {
            IntStepper component = (IntStepper) super.loadComponent(factory, element, parent);
    
            String manualInput = element.attributeValue("manualInput");
            if (manualInput != null) {
                component.setManualInputAllowed(Boolean.valueOf(manualInput));
            }
            String mouseWheel = element.attributeValue("mouseWheel");
            if (mouseWheel != null) {
                component.setMouseWheelEnabled(Boolean.valueOf(mouseWheel));
            }
            String stepAmount = element.attributeValue("stepAmount");
            if (stepAmount != null) {
                component.setStepAmount(Integer.valueOf(stepAmount));
            }
            String maxValue = element.attributeValue("maxValue");
            if (maxValue != null) {
                component.setMaxValue(Integer.valueOf(maxValue));
            }
            String minValue = element.attributeValue("minValue");
            if (minValue != null) {
                component.setMinValue(Integer.valueOf(minValue));
            }
            return component;
        }
    }

    Логика загрузки базовых свойств компонента Field сосредоточена в классе AbstractFieldLoader. Нам достаточно загрузить только специфические свойства IntStepper.

  • Палитра компонентов проекта в модуле web:

    package com.company.myproject.web;
    
    import com.company.myproject.gui.components.IntStepper;
    import com.company.myproject.gui.loaders.IntStepperLoader;
    import com.company.myproject.web.components.WebIntStepper;
    import com.haulmont.cuba.gui.ComponentPalette;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.xml.layout.ComponentLoader;
    import java.util.HashMap;
    import java.util.Map;
    
    public class AppComponentPalette implements ComponentPalette {
    
        @Override
        public Map<String, Class<? extends ComponentLoader>> getLoaders() {
            Map<String, Class<? extends ComponentLoader>> loaders = new HashMap<>();
            loaders.put(IntStepper.NAME, IntStepperLoader.class);
            return loaders;
        }
    
        @Override
        public Map<String, Class<? extends Component>> getComponents() {
            Map<String, Class<? extends Component>> components = new HashMap<>();
            components.put(IntStepper.NAME, WebIntStepper.class);
            return components;
        }
    }
  • Регистрация палитры компонентов в классе App модуля web:

    package com.company.myproject.web;
    
    import com.haulmont.cuba.web.DefaultApp;
    import com.haulmont.cuba.web.gui.WebUIPaletteManager;
    
    public class App extends DefaultApp {
    
        static {
            WebUIPaletteManager.registerPalettes(new AppComponentPalette());
        }
    }
  • XSD компонентов проекта в модуле gui:

    <xs:schema targetNamespace="http://schemas.company.com/app/0.1/app-components.xsd"
               xmlns:xs="http://www.w3.org/2001/XMLSchema"
               xmlns="http://schemas.company.com/app/0.1/app-components.xsd"
               elementFormDefault="qualified"
               attributeFormDefault="unqualified">
    
        <xs:element name="intStepper">
            <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>
  • Использование компонента в экране внутри произвольного контейнера:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
            xmlns:app="http://schemas.company.com/app/0.1/app-components.xsd"
            caption="msg://editCaption"
            class="com.company.myproject.web.customer.CustomerEdit"
            datasource="customerDs"
            focusComponent="fieldGroup"
            messagesPack="com.company.myproject.web.customer">
        <dsContext>
            <datasource id="customerDs"
                        class="com.company.myproject.entity.Customer"
                        view="_local"/>
        </dsContext>
        <layout expand="windowActions"
                spacing="true">
            <app:intStepper id="stepper" datasource="customerDs" property="score" caption="Score"
                            minValue="1" maxValue="20"/>
            <iframe id="windowActions"
                    screen="editWindowActions"/>
        </layout>
    </window>

    В данном примере компонент intStepper подсоединен к атрибуту score сущности Customer, экземпляр которой находится в источнике данных customerDs.

  • Использование компонента в кастомном поле FieldGroup:

    <dsContext>
        <datasource id="customerDs"
                    class="com.company.myproject.entity.Customer"
                    view="_local"/>
    </dsContext>
    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="customerDs">
            <column width="250px">
                <field id="name"/>
                <field id="score" custom="true" caption="Score"/>
            </column>
        </fieldGroup>
    ...
    @Inject
    private ComponentsFactory componentsFactory;
    @Inject
    private FieldGroup fieldGroup;
    
    @Override
    public void init(Map<String, Object> params) {
        fieldGroup.addCustomField("score", new FieldGroup.CustomFieldGenerator() {
            @Override
            public Component generateField(final Datasource datasource, final String propertyId) {
                IntStepper stepper = componentsFactory.createComponent(IntStepper.NAME);
                stepper.setDatasource(datasource, propertyId);
                stepper.setWidth("100%");
                return stepper;
            }
        });
    }

Глава 6. Развертывание приложений

В данной главе рассматриваются различные аспекты развертывания и эксплуатации CUBA-приложений.

На диаграмме ниже приведена возможная структура развернутого приложения.

В приведенном варианте приложение обеспечивает отсутствие единой точки отказа, балансировку нагрузки и подключение различных типов клиентов. В простейшем случае, однако, серверная часть приложения может быть установлена на одном компьютере, содержащем, в том числе, и базу данных. Различные варианты развертывания в зависимости от нагрузки и требований к отказоустойчивости подробно рассмотрены в Раздел 6.3, «Масштабирование приложения».

6.1. Каталоги приложения

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

6.1.1. Конфигурационный каталог

Каталог конфигурации предназначен для размещения ресурсов, дополняющих и переопределяющих свойства приложения, пользовательский интерфейс и бизнес-логику после развертывания приложения. Переопределение обеспечивается механизмом загрузки интерфейса инфраструктуры Resources , который сначала выполняет поиск в конфигурационном каталоге, а потом в CLASSPATH, так что одноименные ресурсы в конфигурационном каталоге имеют приоритет над расположенными в JAR-файлах и каталогах классов.

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

  • Файл local.app.properties , определяющий параметры развертывания блоков приложения, работающих под управлением веб-сервера.

  • Конфигурационные файлы metadata.xml , persistence.xml , views.xml , remoting-spring.xml .

  • XML-дескрипторы экранов UI.

  • Контроллеры экранов UI в виде исходных текстов Java или Groovy.

  • Скрипты или классы Groovy, а также исходные тексты классов Java, используемые приложением через интерфейс Scripting .

Расположение конфигурационного каталога определяется свойством приложения cuba.confDir . Для блоков Middleware, Web Client и Web Portal в варианте быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/conf, например tomcat/conf/app-core для Middleware.

6.1.2. Рабочий каталог

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

Например, подкаталог filestorage рабочего каталога по умолчанию используется хранилищем загруженных файлов. Кроме того, блок Middleware на старте сохраняет в рабочем каталоге сгенерированные файлы persistence.xml и orm.xml.

Расположение рабочего каталога определяется свойством приложения cuba.dataDir . Для блоков Middleware, Web Client и Web Portal в варианте быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/work.

6.1.3. Каталог журналов

В каталоге журналов создаются лог-файлы приложения.

Состав и настройка файлов журналов определяются конфигурацией фреймворка Apache log4j. Расположение файла конфигурации определяется системным свойством log4j.configuration .

Данный каталог может быть также использован для сохранения произвольной информации о выполнении приложения. Путь к каталогу журналов определяется свойством приложения cuba.logDir . Для блоков Middleware, Web Client и Web Portal в варианте быстрого развертывания в Tomcat это каталог tomcat/logs.

См. также Раздел 5.6, «Логгирование».

6.1.4. Временный каталог

Данный каталог может быть использован для создания произвольных временных файлов во время выполнения приложения. Путь к временному каталогу определяется свойством приложения cuba.tempDir . Для блоков Middleware, Web Client и Web Portal в варианте быстрого развертывания в Tomcat это подкаталог с именем веб-приложения в каталоге tomcat/temp.

6.1.5. Каталог скриптов базы данных

В данном каталоге развернутого блока Middleware хранится набор SQL скриптов создания и обновления БД.

Структура каталога скриптов повторяет описанную в Раздел 4.3.2, «Скрипты создания и обновления БД», но имеет один дополнительный верхний уровень, разделяющий скрипты используемых базовых проектов и самого приложения. Нумерация каталогов верхнего уровня определяется во время сборки проекта.

Расположение каталога скриптов БД определяется свойством приложения cuba.dbDir . В варианте быстрого развертывания в Tomcat это подкаталог WEB-INF/db каталога веб-приложения среднего слоя: tomcat/webapps/app-core/WEB-INF/db.

6.2. Варианты развертывания

В данном разделе рассматриваются различные варианты развертывания CUBA-приложений.

6.2.1. Быстрое развертывание в Tomcat

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

Быстрое развертывание производится с помощью задачи deploy, объявленной для модулей core и web в файле build.gradle. Перед первым выполнением deploy необходимо установить и проинициализировать локальный сервер Tomcat с помощью задачи setupTomcat.

В результате быстрого развертывания в каталоге, задаваемом свойством ext.tomcatDir скрипта build.gradle создается следующая структура (перечислены только важные каталоги и файлы, описанные ниже):

bin/
    setenv.bat, setenv.sh
    startup.bat, startup.sh
    debug.bat, debug.sh
    shutdown.bat, shutdown.sh

conf/
    catalina.properties
    server.xml
    log4j.xml
    logging.properties
    Catalina/
        localhost/
    app/
    app-core/

lib/
    hsqldb-2.2.9.jar

logs/
    app.log

shared/
    lib/

temp/
    app/
    app-core/

webapps/
    app/
    app-core/

work/
    app/
    app-core/
  • bin - каталог, содержащий средства запуска и остановки сервера Tomcat:

    • setenv.bat, setenv.sh - скрипты установки переменных окружения. Эти скрипты следует использовать для установки параметров памяти JVM, указания файла конфигурации логгирования, настройки доступа по JMX, параметров подключения отладчика.

    • startup.bat, startup.sh - скрипты запуска Tomcat. Сервер стартует в отдельном консольном окне в Windows и в фоне в *nix.

      Для запуска сервера в текущем консольном окне вместо startup.* используйте команды

      > catalina.bat run

      $ ./catalina.sh run

    • debug.bat, debug.sh - скрипты, аналогичные startup.*, однако запускающие Tomcat с возможностью подключения отладчика. Именно эти скрипты запускаются при выполнении задачи start скрипта сборки.

    • shutdown.bat, shutdown.sh - скрипты остановки Tomcat.

  • conf - каталог, содержащий файлы конфигурации Tomcat и развернутых в нем приложений.

    • catalina.properties - свойства Tomcat. Для загрузки общих библиотек из каталога shared/lib (см. ниже) данный файл должен содержать строку:

      shared.loader=${catalina.home}/shared/lib/*.jar
    • server.xml - описатель конфигурации Tomcat. В этом файле можно изменить порты сервера.

    • log4j.xml - описатель конфигурации логгирования приложений.

    • logging.properties - описатель конфигурации логгирования самого сервера Tomcat.

    • Catalina/localhost - в этом каталоге можно разместить дескрипторы развертывания приложений context.xml. Дескрипторы, расположенные в данном каталоге имеют приоритет над дескрипторами в каталогах META-INF самих приложений, что часто бывает удобно при эксплуатации системы. Например, в таком дескрипторе на уровне сервера можно указать параметры подключения к базе данных, отличные от указанных в самом приложении.

      Дескриптор развертывания на уровне сервера должен иметь имя приложения и расширение .xml. То есть для создания такого дескриптора, например, для приложения app-core, необходимо скопировать содержимое файла webapps/app-core/META-INF/context.xml в файл conf/Catalina/localhost/app-core.xml.

    • app - конфигурационный каталог приложения веб-клиента app.

    • app-core - конфигурационный каталог приложения среднего слоя app-core.

  • lib - каталог библиотек, загружаемых в common classloader сервера. Эти библиотеки доступны как самому серверу, так и всем развернутым в нем веб-приложениям. В частности, в данном каталоге должны располагаться JDBC-драйверы используемых баз данных (hsqldb-XYZ.jar, postgresql-XYZ.jar и т.д.)

  • logs - каталог логов приложений и сервера. Основной лог-файл приложений - app.log.

  • shared/lib - каталог библиотек, доступных всем развернутым приложениям. Классы этих библиотек загружаются в специальный shared classloader сервера. Использование shared classloader задается в файле conf/catalina.properties как описано выше.

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

  • temp/app, temp/app-core - временные каталоги приложений веб-клиента и среднего слоя.

  • webapps - каталог веб-приложений. Каждое приложение располагается в собственном подкаталоге в формате exploded WAR.

    Задачи deploy файла сборки создают подкаталоги приложений с именами, указанными в параметрах appName, и кроме прочего копируют в их подкаталоги WEB-INF/lib библиотеки, перечисленные в параметре jarNames.

  • work/app, work/app-core - рабочие каталоги приложений веб-клиента и среднего слоя.

6.2.1.1. Использование Tomcat при эксплуатации приложения

Процедура быстрого развертывания создает веб приложения app и app-core, работающие на локальном инстансе Tomcat на порту 8080. Это означает, что веб клиент доступен по адресу http://localhost:8080/app. Вы можете использовать этот сервер для эксплуатации приложения, однако необходимо настроить некоторые его свойства.

Сначала установите имя хоста сервера.

Если изменения порта (8080) и веб контекста (app) не требуется, установите следующие свойства приложения в файлах tomcat/conf/app/local.app.properties и tomcat/conf/app-core/local.app.properties:

  cuba.webHostName = myserver
  cuba.webAppUrl = http://myserver:8080/app

Если порт сервера отличается от 8080, установите также свойство cuba.webPort:

  cuba.webPort = 7070
  cuba.webHostName = myserver
  cuba.webAppUrl = http://myserver:7070/app

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

  • Переименуйте каталоги веб приложений и подкаталоги conf:

      tomcat/
          conf/
              sales/
                  local.app.properties
              sales-core/
                  local.app.properties
          webapps/
              sales/
              sales-core/

  • Откройте файл tomcat/webapps/sales-core/WEB-INF/web.xml и измените последнюю строку в значении параметра appPropertiesConfig:

    file:${catalina.home}/conf/sales-core/local.app.properties

  • Откройте файл tomcat/webapps/sales/WEB-INF/web.xml и измените последнюю строку в значении параметра appPropertiesConfig:

    file:${catalina.home}/conf/sales/local.app.properties

  • Добавьте в tomcat/conf/sales-core/local.app.properties:

      cuba.webContextName = sales-core
      cuba.webPort = 7070
      cuba.webHostName = myserver
      cuba.webAppUrl = http://myserver:7070/sales

  • Добавьте в tomcat/conf/sales/local.app.properties:

      cuba.connectionUrlList = http://localhost:7070/sales-core
      cuba.webContextName = sales
      cuba.webPort = 7070
      cuba.webHostName = myserver
      cuba.webAppUrl = http://myserver:7070/sales

    Свойство приложения cuba.connectionUrlList используется для перекачки файлов между веб клиентом и Middleware даже в случае local service invocations, поэтому оно всегда должно указывать на реальный URL веб приложения Middleware.

Если для веб клиента вы хотите использовать корневой контекст (http://myserver:8080), переименуйте каталоги sales в ROOT

  tomcat/
      conf/
          ROOT/
              local.app.properties
          sales-core/
              local.app.properties
      webapps/
          ROOT/
          sales-core/

и используйте / в качестве веб контекста в файле tomcat/conf/ROOT/local.app.properties:

  cuba.webContextName = /

6.2.2. Развертывание в WAR

Стандартное для JavaEE развертывание приложений в WAR-файлы осуществляется с помощью задач сборки buildWar и createWarDistr. Рассмотрим пример сборки WAR-файлов и их развертывания на сервере Glassfish 4.

  1. Добавляем в build.gradle задачи сборки WAR для модулей core и web:

    configure(coreModule) {
        ...
        task buildWar(dependsOn: assemble, type: CubaWarBuilding) {
            appName = 'app-core'
            appHome = '${app.home}'
        }
    }
    
    configure(webModule) {
        ...
        task buildWar(dependsOn: assemble, type: CubaWarBuilding) {
            appName = 'app'
            appHome = '${app.home}'
        }
    }
  2. Добавляем в build.gradle задачу сборки дистрибутива:

    task createWarDistr(dependsOn: [coreModule.buildWar, webModule.buildWar], type: CubaWarDistribution) {
        appHome = '${app.home}'
    }
  3. Запускаем сборку:

    gradlew createWarDistr

    В результате в подкаталоге build/war проекта создаются домашний каталог с именем ${app.home} и файлы app-core.war и app.war. Имя домашнего каталога здесь роли не играет, так как реальное имя будет задаваться для сервера с помощью системной переменной Java.

  4. Копируем содержимое build/war/${app.home} на сервер, например в каталог /home/user/app_home.

  5. Устанавливаем сервер Glassfish 4, например в каталог /home/user/glassfish4.

  6. Копируем JDBC-драйвер используемой базы данных в каталог /home/user/glassfish4/glassfish/domains/domain1/lib. Файл драйвера можно взять из каталога lib Studio, либо из каталога build/tomcat/lib проекта (если перед этим выполнялось быстрое развертывание в Tomcat).

  7. Запускаем сервер:

    $ cd /home/user/glassfish4/bin

    $ ./asadmin start-domain

  8. Переходим по адресу http://localhost:4848 и в консоли управления сервером:

    1. Создаем JDBC Connection Pool для подключения к нашей базе данных, например:

      • Pool Name: AppDB

      • Resource Type: javax.sql.DataSource

      • Database Driver Vendor: Postgresql

      • Datasource Classname: org.postgresql.ds.PGSimpleDataSource

      • User: cuba

      • DatabaseName: app_db

      • Password: cuba

    2. Создаем JDBC Resource:

      • JNDI Name: jdbc/CubaDS

      • Pool Name: AppDB

    3. В экране server (Admin Server) -> Properties -> System Properties задаем следующие системные переменные Java:

      • app.home = /home/user/app_home - домашний каталог приложения.

      • log4j.configuration = file:///home/user/app_home/log4j.xml - файл конфигурации логгирования приложения.

  9. Перезапускаем сервер:

    $ ./asadmin stop-domain

    $ ./asadmin start-domain

  10. Снова открываем консоль сервера по адресу http://localhost:4848 и в экране Applications выполняем развертывание файлов app-core.war и app.war, находящихся в каталоге дистрибутива, созданного на шаге 3.

  11. Приложение запущено:

    • Веб-интерфейс доступен по адресу http://localhost:8080/app

    • Лог-файлы создаются в каталоге /home/user/app_home/logs

6.3. Масштабирование приложения

В данном разделе рассмотрены способы масштабирования CUBA-приложения, состоящего из блоков Middleware и Web Client, при возрастании нагрузки и ужесточении требований к отказоустойчивости.

Этап 1. Оба блока развернуты на одном сервере приложения.

Это простейший вариант, реализуемый стандартной процедурой быстрого развертывания.

В данном случае обеспечивается максимальная производительность передачи данных между блоками Web Client и Middleware, так как при включенном свойстве приложения cuba.useLocalServiceInvocation сервисы Middleware вызываются в обход сетевого стека.

Этап 2. Блоки Middleware и Web Client развернуты на отдельных серверах приложения.

Данный вариант позволяет распределить нагрузку между двумя серверами приложения и более оптимально использовать ресурсы серверов. Кроме того, в этом случае нагрузка от веб-пользователей меньше сказывается на выполнении других процессов. Под другими процессами здесь понимается обслуживание средним слоем других типов клиентов (например Desktop), выполнение задач по расписанию и, возможно, интеграционные задачи.

Требования к ресурсам серверов:

  • Tomcat 1 (Web Client):

    • Объем памяти - пропорционально количеству одновременно подключенных пользователей.

    • Мощность CPU - зависит от интенсивности работы пользователей.

  • Tomcat 2 (Middleware):

    • Объем памяти - фиксированный и относительно небольшой.

    • Мощность CPU - зависит от интенсивности работы пользователей и других процессов.

В этом и более сложных вариантах развертывания в блоке Web Client свойство приложения cuba.useLocalServiceInvocation должно быть установлено в false, а свойство cuba.connectionUrlList должно содержать URL блока Middleware.

Этап 3. Кластер серверов Web Client работает с одним сервером Middleware.

Данный вариант применяется, когда вследствие большого количества одновременно подключенных пользователей требования к памяти для блока Web Client превышают возможности одной JVM. В этом случае запускается кластер (два или более) серверов Web Client, и подключение пользователей производится через Load Balancer. Все серверы Web Client работают с одним сервером Middleware.

Дублирование серверов Web Client автоматически обеспечивает отказоустойчивость на этом уровне. Однако, так как репликация HTTP-сессий не поддерживается, при незапланированном отключении одного из серверов Web Client все пользователи, подключенные к нему, вынуждены будут выполнить новый логин в приложение.

Настройка данного варианта развертывания описана в Раздел 6.3.1, «Настройка кластера Web Client».

Этап 4. Кластер серверов Web Client работает с кластером серверов Middleware.

Это максимальный вариант развертывания, обеспечивающий отказоустойчивость и балансировку нагрузки для Middleware и Web Client.

Подключение пользователей к серверам Web Client производится через Load Balancer. Серверы WebClient работают с кластером серверов Middleware. Для этого им не требуется дополнительный Load Balancer - достаточно определить список URL серверов Middleware в свойстве cuba.connectionUrlList.

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

Настройка данного варианта развертывания описана в Раздел 6.3.2, «Настройка кластера Middleware».

6.3.1. Настройка кластера Web Client

В данном разделе рассматривается следующая конфигурация развертывания:

Здесь на серверах host1 и host2 блок установлены инстансы Tomcat с веб-приложением app, реализующим блок Web Client. Пользователи обращаются к балансировщику нагрузки по адресу http://host0/app, который перенаправляет запрос этим серверам. На сервере host3 установлен Tomcat с веб-приложением app-core, реализующим блок Middleware.

6.3.1.1. Установка и настройка Load Balancer

Рассмотрим процесс установки балансировщика нагрузки на базе Apache HTTP Server для операционной системы Ubuntu 14.04.

  1. Выполните установку Apache HTTP Server и его модуля mod_jk:

    $ sudo apt-get install apache2 libapache2-mod-jk

  2. Замените содержимое файла /etc/libapache2-mod-jk/workers.properties на следующее:

    workers.tomcat_home=
    workers.java_home=
    ps=/
    
    worker.list=tomcat1,tomcat2,loadbalancer,jkstatus
    
    worker.tomcat1.port=8009
    worker.tomcat1.host=host1
    worker.tomcat1.type=ajp13
    worker.tomcat1.connection_pool_timeout=600
    worker.tomcat1.lbfactor=1
    
    worker.tomcat2.port=8009
    worker.tomcat2.host=host2
    worker.tomcat2.type=ajp13
    worker.tomcat2.connection_pool_timeout=600
    worker.tomcat2.lbfactor=1
    
    worker.loadbalancer.type=lb
    worker.loadbalancer.balance_workers=tomcat1,tomcat2
    
    worker.jkstatus.type=status
  3. Добавьте в файл /etc/apache2/sites-available/000-default.conf следующее:

    <VirtualHost *:80>
    ...
        <Location /jkmanager>
            JkMount jkstatus
            Order deny,allow
            Allow from all
        </Location>
    
        JkMount /jkmanager/* jkstatus
        JkMount /app loadbalancer
        JkMount /app/* loadbalancer
    
    </VirtualHost>

  4. Перезапустите сервис Apache HTTP:

    $ sudo service apache2 restart

6.3.1.2. Настройка серверов Web Client

На серверах Tomcat 1 и Tomcat 2 необходимо произвести следующие настройки:

  1. В файлах tomcat/conf/server.xml добавить параметр jvmRoute, эквивалентный имени worker, заданному в настройках балансировщика нагрузки - tomcat1 и tomcat2:

    <Server port="8005" shutdown="SHUTDOWN">
      ...
      <Service name="Catalina">
        ...
        <Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
          ...
        </Engine>
      </Service>
    </Server>
  2. Задать следующие свойства приложения в файлах tomcat/conf/app/local.app.properties:

    cuba.useLocalServiceInvocation = false
    cuba.connectionUrlList = http://host3:8080/app-core
    
    cuba.webHostName = host1
    cuba.webPort = 8080
    cuba.webContextName = app

    Параметры cuba.webHostName, cuba.webPort, cuba.webContextName не обязательны для работы кластера WebClient, но позволяют проще идентифицировать сервера в других механизмах платформы, например в консоли JMX. Кроме того, в экране User Sessions в атрибуте Client Info отображается сформированный из этих параметров идентификатор блока Web Client, на котором работает данный пользователь.

6.3.2. Настройка кластера Middleware

В данном разделе рассматривается следующая конфигурация развертывания:

Здесь на серверах host1 и host2 блок установлены инстансы Tomcat с веб-приложением app, реализующим блок Web Client. Настройка кластера этих серверов рассмотрена в предыдущем разделе. На серверах host3 и host4 установлены инстансы Tomcat с веб-приложением app-core, реализующим блок Middleware. Между ними настроено взаимодействие для обмена информацией о пользовательских сессиях и блокировках, сброса кэшей и др.

6.3.2.1. Настройка обращения к кластеру Middleware

Для того, чтобы клиентские блоки могли работать с несколькими серверами Middleware, достаточно указать список URL этих серверов в свойстве приложения cuba.connectionUrlList. Для Web Client это можно сделать в файле tomcat/conf/app/local.app.properties:

cuba.useLocalServiceInvocation = false
cuba.connectionUrlList = http://host3:8080/app-core,http://host4:8080/app-core

cuba.webHostName = host1
cuba.webPort = 8080
cuba.webContextName = app

Порядок серверов в списке cuba.connectionUrlList определяет приоритет, в котором клиент будет пытаться направлять запросы. Например в данном случае клиент сначала попытается вызвать host1, если он недоступен - то host2. Если запрос к host2 завершился успешно, данный клиент ставит host2 первым в своем списке и продолжает работать с ним. После перезапуска клиента список восстанавливается в первоначальное значение. Для обеспечения равномерного распределения клиентов между серверами используется свойство cuba.randomServerPriority.

6.3.2.2. Настройка взаимодействия серверов Middleware

Сервера Middleware могут поддерживать общие списки пользовательских сессий и других объектов, а также координировать сброс кэшей. Для этого достаточно на каждом их них включить свойство приложения cuba.cluster.enabled. Пример файла tomcat/conf/app-core/local.app.properties:

cuba.cluster.enabled = true

cuba.webHostName = host3
cuba.webPort = 8080
cuba.webContextName = app-core

Для серверов Middleware обязательно нужно указать правильные значения свойств cuba.webHostName, cuba.webPort и cuba.webContextName для формирования уникального Server Id.

Механизм взаимодействия основан на библиотеке JGroups. Для тонкой настройки взаимодействия служит файл jgroups.xml, расположенный в корне архива cuba-core-<version>.jar. Его можно скопировать в каталог tomcat/conf/app-core и настроить нужным образом.

Программный интерфейс для взаимодействия в кластере Middleware обеспечивает бин ClusterManagerAPI. Его можно использовать в приложении - см. JavaDocs и примеры использования в коде платформы.

6.3.3. Server Id

Server Id служит для надежной идентификации серверов в кластере Middleware. Идентификатор имеет вид host:port/context, например:

tezis.haulmont.com:80/app-core
192.168.44.55:8080/app-core

Идентификатор формируется на основе параметров конфигурации cuba.webHostName , cuba.webPort , cuba.webContextName , поэтому крайне важно корректно указать эти параметры для блока Middleware, работающего в кластере.

Server Id может быть получен c помощью бина ServerInfoAPI или через JMX-интерфейс ServerInfoMBean .

6.4. Использование инструментов JMX

В данном разделе рассмотрены различные аспекты использования инструментов Java Management Extensions в CUBA-приложениях.

6.4.1. Встроенная JMX консоль

Модуль Web Client базового проекта cuba платформы содержит средство просмотра и редактирования JMX объектов. Точкой входа в этот инструмент является экран com/haulmont/cuba/web/app/ui/jmxcontrol/browse/display-mbeans.xml, зарегистрированный под идентификатором jmxConsole и в стандартном меню доступный через пункт Администрирование -> Консоль JMX.

Без дополнительной настройки консоль отображает все JMX объекты, зарегистрированные в JVM, на которой работает блок Web Client, к которому в данный момент подключен пользователь. Соответственно, в простейшем случае развертывания всех блоков приложения в одном экземпляре веб-контейнера консоль имеет доступ к JMX бинам всех уровней, а также к JMX объектам самой JVM и веб-контейнера.

Имена бинов приложения имеют префикс, соответствующий имени веб-приложения, их содержащего. Например, бин app-core.cuba:type=CachingFacade загружен веб-приложением app-core, реализующим блок Middleware, а бин app.cuba:type=CachingFacade загружен веб-приложением app, реализующим блок Web Client.

Консоль JMX может также работать с JMX объектами произвольной удаленной JVM. Это актуально в случае развертывания блоков приложения на нескольких экземплярах веб-контейнера, например, отдельно Web Client и Middleware.

Для подключения к удаленной JVM необходимо в поле Соединение JMX консоли выбрать созданное ранее соединение, либо вызвать экран создания нового соединения:

Рисунок 47. Редактирование JMX соединения

Редактирование JMX соединения

Для соединения указывается JMX хост и порт, логин и пароль. Имеется также поле Имя узла, которое заполняется автоматически, если по указанному адресу обнаружен какой-либо блок CUBA-приложения. В этом случае значением этого поля становится комбинация свойств cuba.webHostName и cuba.webPort данного блока, что позволяет идентифицировать содержащий его сервер. Если подключение произведено к постороннему JMX интерфейсу, то поле Имя узла будет иметь значение "Unknown JMX interface". Значение данного поля можно произвольно изменять.

Для подключения удаленной JVM она должна быть соответствующим образом настроена - см. ниже.

6.4.2. Настройка удаленного доступа к JMX

В данном разделе рассматривается настройка запуска сервера Tomcat, необходимая для удаленного подключения к нему инструментов JMX.

6.4.2.1. Tomcat JMX под Windows

  • Отредактировать файл bin/setenv.bat следующим образом:

    set CATALINA_OPTS=%CATALINA_OPTS% ^
    -Dcom.sun.management.jmxremote ^
    -Djava.rmi.server.hostname=192.168.10.10 ^
    -Dcom.sun.management.jmxremote.ssl=false ^
    -Dcom.sun.management.jmxremote.port=7777 ^
    -Dcom.sun.management.jmxremote.authenticate=true ^
    -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password ^
    -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access

    Здесь в параметре java.rmi.server.hostname необходимо указать реальный IP адрес или DNS имя компьютера, на котором запущен сервер, в параметре com.sun.management.jmxremote.port - порт для подключения инструментов JMX.

  • Отредактировать файл conf/jmxremote.access. Он должен содержать имена пользователей, которые будут подключаться к JMX, и их уровень доступа. Например:

    admin readwrite
  • Отредактировать файл conf/jmxremote.password. Он должен содержать пароли пользователей JMX, например:

    admin admin
  • Файл паролей должен иметь разрешение на чтение только для пользователя, от имени которого работает сервер Tomcat. Настроить права можно следующим образом:

    • Открыть командную строку и перейти в каталог conf.

    • Выполнить команду:

      cacls jmxremote.password /P "domain_name\user_name":R

      где domain_name\user_name - домен и имя пользователя.

    • После выполнения данной команды файл в Проводнике будет отмечен изображением замка.

  • Если Tomcat установлен как служба Windows, то для службы должен быть задан вход в систему с учетной записью, имеющей права на файл jmxremote.password. Кроме того, следует иметь в виду, что в этом случае файл bin/setenv.bat не используется, и соответствующие параметры запуска JVM должны быть заданы в приложении, настраивающем службу.

6.4.2.2. Tomcat JMX под Linux

  • Отредактировать файл bin/setenv.sh следующим образом:

    CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote \
    -Djava.rmi.server.hostname=192.168.10.10 \
    -Dcom.sun.management.jmxremote.port=7777 \
    -Dcom.sun.management.jmxremote.ssl=false \
    -Dcom.sun.management.jmxremote.authenticate=true"
    
    CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"

    Здесь в параметре java.rmi.server.hostname необходимо указать реальный IP адрес или DNS имя компьютера, на котором запущен сервер, в параметре com.sun.management.jmxremote.port - порт для подключения инструментов JMX.

  • Отредактировать файл conf/jmxremote.access. Он должен содержать имена пользователей, которые будут подключаться к JMX, и их уровень доступа. Например:

    admin readwrite
  • Отредактировать файл conf/jmxremote.password. Он должен содержать пароли пользователей JMX, например:

    admin admin
  • Файл паролей должен иметь разрешение на чтение только для пользователя, от имени которого работает сервер Tomcat. Настроить права для текущего пользователя можно следующим образом:

    • Открыть командную строку и перейти в каталог conf.

    • Выполнить команду:

      chmod go-rwx jmxremote.password

6.5. Создание и обновление БД при эксплуатации приложения

В данном разделе рассматриваются способы создания и обновления базы данных на этапе развертывания и эксплуатации приложения. Для знакомства с устройством и правилами создания скриптов БД см. Раздел 4.3.2, «Скрипты создания и обновления БД» и Раздел 5.5.1, «Создание схемы БД».

6.5.1. Использование механизма выполнения скриптов БД сервером

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

Чтобы инициализировать новую базу данных, нужно выполнить следующее:

  • включить свойство приложения cuba.automaticDatabaseUpdate , добавив следующую строку в файл local.app.properties :

    cuba.automaticDatabaseUpdate = true
  • создать пустую базу данных, соответствующую URL, заданному в описании источника данных в context.xml

  • запустить сервер приложения, содержащий блок Middleware. На старте приложения БД будет проинициализирована и сразу же готова к работе.

В дальнейшем при каждом старте сервера приложения механизм выполнения скриптов будет сравнивать набор скриптов, находящийся в каталоге скриптов базы данных, со списком выполненных скриптов, зарегистрированным в БД. При появлении в каталоге новых скриптов они будут выполнены и также зарегистрированы. Таким образом, достаточно в каждую новую версию приложения включать скрипты обновления, и при рестарте сервера приложения база данных будет приводиться в актуальное состояние.

При эксплуатации механизма выполнения скриптов на старте сервера следует иметь в виду следующее:

  • При любой ошибке выполнения скрипта блок Middleware прерывает инициализацию и становится неработоспособным. Клиентские блоки выдают сообщения о невозможности подключения к Middleware.

    Для выяснения причин сбоя необходимо открыть файл лога app.log в каталоге журналов сервера и найти сообщения о выполнении SQL от логгера com.haulmont.cuba.core.sys.DbUpdaterEngine, и, возможно, последующие сообщения об ошибках.

  • Скрипты обновления, а также отделенные символом "^" команды DDL и SQL внутри скриптов выполняются в отдельных транзакциях. Поэтому при возникновении ошибки при обновлении существует большая вероятность того, что часть скриптов, или даже отдельных команд последнего скрипта, выполнилась и зафиксирована в БД.

    В связи с этим рекомендуется непосредственно перед запуском сервера с новой версией приложения делать резервное сохранение БД. Тогда после устранения причины ошибки достаточно восстановить БД и запустить автоматический процесс вновь.

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

    CUBA Studio генерирует скрипты обновления с символом ";" в качестве разделителями для всех типов БД, кроме Oracle. Если команды скрипта разделены точками с запятой, они выполняются в одной транзакции, и в случае ошибки скрипт откатывается целиком. Тем самым обеспечивается постоянное соответствие между структурой БД и списком выполненных скриптов обновления.

6.5.2. Инициализация и обновление БД из командной строки

Скрипты создания и обновления БД могут быть запущены из командной строки с помощью класса com.haulmont.cuba.core.sys.utils.DbUpdaterUtil, входящего в состав блока Middleware платформы. При запуске должны быть переданы следующие аргументы:

  • dialect - тип СУБД, возможные значения: postgres, mssql, oracle.

  • dbUser - имя пользователя БД.

  • dbPassword - пароль пользователя БД.

  • dbUrl - URL для подключения к БД. Для выполнения первичной инициализации указанная база данных должна быть пустой, никакой предварительной очистки ее не производится.

  • scriptsDir - абсолютный путь к каталогу, содержащему скрипты в стандартной структуре. Как правило, используется каталог скриптов базы данных, поставляемый с приложением.

  • одна из возможных команд:

    • create - выполнить инициализацию базы данных.

    • check - отобразить список невыполненных скриптов обновления.

    • update - выполнить обновление базы данных.

Пример скрипта для Linux, запускающего DbUpdaterUtil:

#!/bin/sh

DB_URL="jdbc:postgresql://localhost/mydb"

APP_CORE_DIR="./../webapps/app-core"
WEBLIB="$APP_CORE_DIR/WEB-INF/lib"
SCRIPTS="$APP_CORE_DIR/WEB-INF/db"
TOMCAT="./../lib"
SHARED="./../shared/lib"

CLASSPATH=""
for jar in `ls "$TOMCAT/"`
do
  CLASSPATH="$TOMCAT/$jar:$CLASSPATH"
done

for jar in `ls "$WEBLIB/"`
do
  CLASSPATH="$WEBLIB/$jar:$CLASSPATH"
done

for jar in `ls "$SHARED/"`
do
  CLASSPATH="$SHARED/$jar:$CLASSPATH"
done

java -cp $CLASSPATH com.haulmont.cuba.core.sys.utils.DbUpdaterUtil \
 -dialect postgres -dbUrl $DB_URL \
 -dbUser $1 -dbPassword $2 \
 -scriptsDir $SCRIPTS \
 -$3

Данный скрипт рассчитан на работу с БД с именем mydb, расположенной на локальном сервере PostgreSQL. Скрипт должен быть расположен в каталоге bin сервера Tomcat, и запускаться с параметрами {имя пользователя}, {пароль}, {команда}, например:

./dbupdate.sh cuba cuba123 update

Ход выполнения скриптов отображается в консоли. При возникновении ошибок обновления следует поступать так же, как описано в предыдущем разделе для механизма автоматического обновления.

При обновлении БД из командной строки имеющиеся Groovy-скрипты запускаются, но реально отрабатывает только их основная часть. По причине отсутствия контекста сервера PostUpdate-часть игнорируется с выдачей в консоль соответствующего сообщения.

6.6. Использование файла лицензии

Вместе с платформой поставляется файл бесплатной лицензии cuba.license, доступный в корне classpath. Свойство приложения cuba.licensePath по умолчанию указывает на этот файл.

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

  1. Если вы планируете использовать приложение в рамках одной организации, или вы получили встраиваемую лицензию, включите файл лицензии в дистрибутив. Это можно сделать путем добавления файла в каталог исходников модуля core. Имя или путь к файлу должны отличаться от /cuba.license:

    modules/core/src/
      myapp-cuba.license
      app.properties

    Установите свойство приложения cuba.licensePath в файле app.properties модуля core:

    cuba.licensePath = /myapp-cuba.license

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

    tomcat/conf/app-core/
      myapp-cuba.license
      local.app.properties

    Установите свойство приложения cuba.licensePath в файле local.app.properties:

    cuba.licensePath = /myapp-cuba.license

Глава 7. Подсистема безопасности

Платформа CUBA включает в себя следующие средства разграничения прав доступа пользователей к информации:

  • Система назначения пользователям разрешений, основанная на ролях; при этом набор ролей и разрешений настраивается администратором системы на этапе внедрения.

  • Иерархическая структура групп доступа с наследованием ограничений.

  • Контроль доступа на следующих уровнях:
    • Операции над сущностями предметной области (чтение, создание, изменение, удаление): например, пользователь Иванов может просматривать документы, но не может создавать, изменять и удалять их.

    • Атрибуты сущностей (изменение, чтение, запрет): пользователь Иванов видит все атрибуты документов, кроме суммы.

    • Доступ к определенным экземплярам сущностей (контроль доступа на уровне строк): пользователь Иванов видит только те документы, которые были созданы в его отделе.

  • Интеграция с LDAP с возможностью реализации технологии единого входа (Single Sign-On) для пользователей Windows.

7.1. Компоненты подсистемы безопасности

Основные компоненты подсистемы безопасности CUBA приведены на следующей диаграмме.

Рисунок 48. Диаграмма компонентов подсистемы безопасности

Диаграмма компонентов подсистемы безопасности

Рассмотрим их более подробно.

Security management screens - имеющиеся в платформе экраны, с помощью которых администратором системы осуществляется настройка прав доступа пользователей.

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

После входа пользователя в систему создается объект UserSession − пользовательская сессия. Это центральный элемент обеспечения безопасности, объект, ассоциированный с аутентифицированным в данный момент в системе пользователем и содержащий информацию о правах доступа пользователя к данным.

Процесс входа пользователя в систему подробно описан в разделе Раздел 4.2.10.2, «Вход в систему».

Roles − роли пользователей. Роль - это объект системы, которому с одной стороны сопоставляется набор разрешений, необходимых для выполнения конкретных функций, а с другой стороны − подмножество пользователей, которые должны иметь эти разрешения.

Разрешения бывают следующих типов:

  • Screen Permissions - возможность открытия некоторого экрана.

  • Entity Operation Permissions - возможность совершения операции с некоторой сущностью: чтение, создание, модификация, удаление.

  • Entity Attribute Permissions - доступ к произвольному атрибуту некоторой сущности: модификация, только чтение, нет доступа.

  • Specific Permissions - разрешение на некоторую именованную функциональность.

  • UI Permissions - управление доступом к элементам некоторого экрана.

Access Groups - группы доступа пользователей. Группы представляют собой иерархическую структуру, каждый элемент которой задает набор ограничений (Constraints), позволяющих контролировать доступ на уровне отдельных экземпляров (строк таблицы) некоторой сущности. Например, пользователь видит только те документы, которые были созданы в его отделе.

7.1.1. Окно входа в систему

Окно входа в систему (Login screen) предназначено для регистрации пользователя путем ввода логина и пароля.

Логин не чувствителен к регистру вводимых символов.

Класс экрана в блоке Web Client - LoginWindow, в блоке Desktop Client - LoginDialog. Для расширения функциональности в приложении можно создать собственных наследников этих классов и возвращать соответствующие экземпляры, переопределив:

Управлять отображением флажка Remember Me в веб клиенте можно с помощью свойства приложения cuba.web.rememberMeEnabled.

Стандартное окно входа содержит также выпадающий список поддерживаемых системой языков. Отображение списка и его содержимое определяются комбинацией свойств приложения cuba.localeSelectVisible и cuba.availableLocales.

7.1.2. Пользователи

Для каждого пользователя системы создается соответствующий экземпляр сущности sec$User. Он содержит уникальный логин, хэш пароля, ссылку на группу доступа, список ролей и другие атрибуты. Управление пользователями осуществляется с помощью экрана Administration -> Users:

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

  • Copy - быстрое создание нового пользователя на основе выбранного. Новый пользователь будет иметь такую же группу доступа и набор ролей. И то и другое можно изменить в появляющемся экране редактирования нового пользователя.

  • Copy settings - позволяет скопировать выбранным пользователям настройки интерфейса, сделанные каким-либо другим пользователем. Настройки интерфейса включают в себя представления таблиц, положение разделителей контейнеров SplitPanel, наборы фильтров и папок поиска.

  • Change password - позволяет администратору системы задать новый пароль выбранному пользователю.

  • Reset passwords - позволяет произвести следующие действия над выбранными пользователями:

    • Если в появляющемся окне Reset passwords for selected users не включать флажок Generate new passwords, то пользователям будет установлен признак Change password at next logon. При следующем успешном логине пользователя ему будет предложено сменить свой пароль.

    • Если в окне Reset passwords for selected users включить флажок Generate new passwords, то для выбранных пользователей будут сгенерированы и показаны новые случайные пароли. Список новых паролей можно выгрузить в формат XLS, и например, разослать пользователям. Кроме того, для каждого пользователя будет установлен признак Change password at next logon, что делает сгенерированный пароль одноразовым.

    • Если в дополнение к Generate new passwords включить флажок Send emails with generated passwords, то новые одноразовые пароли не будут показаны администратору, а автоматически разосланы соответствующим пользователям на их адреса email.

Рассмотрим экран редактирования пользователя:

  • Login - обязательный к заполнению уникальный логин пользователя.

  • Group - группа доступа.

  • Last name, First name, Middle name - части полного имени пользователя.

  • Name - полное имя пользователя. Автоматически формируется на основе вводимых частей (Last, First, Middle) и правила, заданного свойством приложения cuba.user.fullNamePattern. Может быть произвольно изменено вручную.

  • Position - должность.

  • Language - язык интерфейса, устанавливаемый для пользователя, если возможность выбирать язык при входе в систему отключена при помощи свойства приложения cuba.localeSelectVisible.

  • Time Zoneчасовой пояс, в соответствии с которым будут отображаться и вводиться значения типа timestamp.

  • Email - адрес email.

  • Active - если данный флаг не установлен, то пользователь не может войти в систему.

  • Permitted IP Mask - маска разрешенных IP-адресов, с которых возможен вход в систему.

    Маска представляет собой список адресов через запятую. Поддерживаются как адреса формата IPv4, так и адреса формата IPv6. В первом случае адрес должен состоять из четырех чисел, разделенных точками, при этом любая часть вместо числа может содержать знак "*", что означает "любое число". Адрес в формате IPv6 представляет собой восемь групп по четыре шестнадцатеричные цифры, разделенных двоеточием. Любая группа также может быть заменена знаком "*".

    Маска может содержать адреса только одного формата. Наличие адресов формата IPv4 и IPv6 одновременно недопустимо.

    Пример: 192.168.*.*

  • Roles - список ролей пользователя.

  • Substituted Users - список замещаемых пользователей.

7.1.2.1. Замещение пользователей

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

В прикладном коде для получения текущего пользователя рекомендуется использовать метод UserSession.getCurrentOrSubstitutedUser() возвращающий либо замещаемого пользователя, либо пользователя, выполнившего логин (если замещения в данный момент нет).

В то же время механизмы аудита платформы (атрибуты createdBy и updatedBy, журнал изменений и снимки сущностей) всегда регистрируют пользователя, который произвел логин, а не замещаемого пользователя.

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

При выборе другого пользователя в этом списке все открытые экраны будут закрыты, и произойдет замещение. После этого метод UserSession.getUser() по прежнему будет возвращать пользователя, выполнившего логин в систему, а метод UserSession.getSubstitutedUser() - замещенного пользователя. Если замещения нет, метод UserSession.getSubstitutedUser() возвращает null.

Управление замещаемыми пользователями производится с помощью таблицы Substituted Users экрана редактирования пользователя. Рассмотрим экран добавления замещаемого пользователя:

  • User - текущий редактируемый пользователь. Он будет замещать другого пользователя.

  • Substituted user - замещаемый пользователь.

  • Start date, End date - необязательный период замещения. Вне периода замещение будет недоступным. Если период не указан, замещение доступно, пока не удалена данная запись таблицы.

7.1.3. Часовой пояс

Все значения даты и времени по умолчанию отображаются в соответствии с часовым поясом сервера. Часовой пояс сервера возвращается методом TimeZone.getDefault() блока приложения. По умолчанию, платформа получает часовой пояс из операционной системы, однако его можно явно задать системным свойством Java user.timezone. Например, чтобы задать часовой пояс по Гринвичу для веб-клиента и Middleware, работающих на сервере Tomcat под Unix, нужно добавить в файл tomcat/bin/setenv.sh следующее свойство:

CATALINA_OPTS="$CATALINA_OPTS -Duser.timezone=GMT"

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

  • Администратор может задать часовой пояс в экране редактирования пользователя.

  • Пользователь может задать свой часовой пояс в окне Help > Settings.

В обоих случаях, часовой пояс настраивается при помощи двух полей:

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

  • Флажок Auto указывает, что часовой пояс будет получен из текущего окружения (для веб-клиента - из веб-браузера, для для десктоп-клиента - из операционной системы).

Если оба поля пусты, часовые пояса для пользователя не конвертируются. В противном случае, платформа сохраняет часовой пояс в объекте UserSession при логине и использует его для ввода и отображения значений типа timestamp. Значение, возвращаемое методом UserSession.getTimeZone() может также использоваться и в прикладном коде.

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

Преобразование часовых поясов выполняется только для атрибутов типа DateTimeDatatype, то есть, содержащих timestamp. Атрибуты, хранящие только дату (DateDatatype) или время (TimeDatatype) по отдельности, не конвертируются. Вы можете запретить преобразование отдельных timestamp-атрибутов, установив для них аннотацию @IgnoreUserTimeZone.

7.1.4. Разрешения

Разрешение определяет право пользователя на какой-либо объект или функциональность системы: экран, операцию над сущностью и так далее. Разрешение в зависимости от своего значения может как дать пользователю право на объект, так и отобрать его (то есть по сути являться запрещением).

Если явного разрешения на объект не установлено, пользователь имеет право на этот объект.

Разрешения представляются экземплярами сущности sec$Permission и содержат следующие атрибуты:

  • type - тип разрешения: определяет, на какой тип объектов накладывается разрешение.

  • target - конкретный объект разрешения. Формат представления объекта зависит от типа разрешения.

  • value - значение разрешения. Диапазон значений зависит от типа разрешения.

Рассмотрим типы разрешений:

  • PermissionType.SCREEN - разрешение на экран системы.

    В атрибуте target указывается идентификатор экрана, атрибут value может иметь значения 0 или 1 (экран запрещен или разрешен соответственно).

    Права на экраны проверяются при построении главного меню системы и при каждом вызове методов openWindow(), openEditor(), openLookup() интерфейса IFrame.

    Для проверки права на экран в прикладном коде используйте метод isScreenPermitted() интерфейса Security.

  • PermissionType.ENTITY_OP - разрешение на операцию c сущностью.

    В атрибуте target указывается имя сущности и через символ ":" имя операции: create, read, update, delete. Например: library$Book:delete. Атрибут value может иметь значения 0 или 1 (операция запрещена или разрешена соответственно).

    Права на операции с сущностью проверяются при работе с данными через DataManager, а также в связанных с данными визуальных компонентах и стандартных действиях со списками сущностей. В результате права на операции оказывают влияние на поведение клиентских блоков и REST API. При работе с данными непосредственно на Middleware через EntityManager права не проверяются.

    Для проверки права на операцию c сущностью в прикладном коде используйте метод isEntityOpPermitted() интерфейса Security.

  • PermissionType.ENTITY_ATTR - разрешение на атрибут сущности.

    В атрибуте target указывается имя сущности и через символ ":" имя арибута, например: library$Book:name. Атрибут value может иметь значения 0, 1 или 2 (атрибут скрыт, только для чтения, или полностью разрешен соответственно).

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

    Для проверки права на атрибут сущности в прикладном коде используйте метод isEntityAttrPermitted() интерфейса Security.

  • PermissionType.SPECIFIC - разрешение на произвольную именованную функциональность.

    В атрибуте target указывается код функциональности, атрибут value может иметь значения 0 или 1 (запрещено или разрешено соответственно).

    Набор специфических разрешений для данного проекта задается в конфигурационном файле permissions.xml.

    Пример использования:

    @Inject
    private Security security;
    
    private void calculateBalance() {
        if (!security.isSpecificPermitted("myapp.calculateBalance"))
            return;
        ...
    }
  • PermissionType.UI - разрешение на произвольный компонент экрана.

    В атрибуте target указывается идентификатор экрана и через символ ":" путь к компоненту. Описание формата пути см. в следующем разделе.

Для проверки разрешений вместо непосредственного использования методов класса UserSession рекомендуется использовать аналогичные методы интерфейса Security, принимающие во внимание возможное расширение сущностей.

7.1.5. Роли

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

Пользователь может иметь несколько ролей. При этом он получает логическую сумму (ИЛИ) прав на некоторый объект от всех ролей, которые у него есть. Например, если пользователю назначены роли A, B и C, роль A запрещает X, роль B разрешает X, роль C не устанавливает явных разрешений на X, то в итоге X будет разрешен.

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

Если пользователю дать единственную роль без явно установленных разрешений, или не давать никаких ролей вообще, то у него будут все права на все объекты.

Список ролей отображается экраном Administration -> Roles. Здесь помимо стандартных действий создания, изменения и удаления записей имеется кнопка Assign to users, позволяющая назначить выбранную роль сразу нескольким пользователям.

Рассмотрим экран редактирования роли. В верхней его части отображаются атрибуты роли:

  • Name - обязательное уникальное имя (или код) роли. Не может быть изменено после создания.

  • Localized name - понятное пользователю название роли.

  • Description - произвольное описание роли.

  • Type - тип роли, может быть следующим:

    • Standard - в роли данного типа действуют только явно назначенные разрешения.

    • Super - роль данного типа автоматически дает все разрешения. Это удобно для назначения администраторов системы, так как она отменяет все запрещения, установленные другими ролями.

    • Read-only - роль данного типа автоматически отнимает разрешения на следующие операции с сущностями: CREATE, UPDATE, DELETE. Таким образом, пользователь с такой ролью может только читать данные, и не может их изменять (если какая-либо другая роль этого пользователя не разрешает явно эти операции).

    • Denying - запрещающая роль. Роль данного типа автоматически отнимает разрешения на все объекты, кроме атрибутов сущностей. Чтобы пользователь с данной ролью мог что-то увидеть или изменить в системе, ему нужно назначить дополнительно другую роль, явно дающую нужные права.

    Роли всех типов могут иметь явно установленные разрешения, например в Read-only роль можно добавить разрешения на модификацию некоторых сущностей. Однако для роли Super явная установка каких-либо запрещений не имеет смысла, так как наличие роли данного типа в любом случае отменяет все запрещения.

    Пользователь с запрещающей ролью не сможет войти в десктоп или веб клиент, так как роль данного типа отнимает также специфическое разрешение cuba.gui.loginToClient (отображаемое в списке специфических разрешений как "Login to client"). Поэтому необходимо дать это разрешение пользователям явно - в какой-либо другой роли, либо прямо в запрещающей.

  • Default role - признак роли по умолчанию. Все роли с данным признаком автоматически назначаются вновь создаваемым пользователям.

Ниже отображаются вкладки управления разрешениями.

  • Вкладка Screens - разрешения на экраны системы:

    Дерево в левой части вкладки отражает структуру главного меню системы. Последним элементом дерева является Other screens, внутри которого сосредоточены экраны, не включенные в главное меню (например, экраны редактирования сущностей).

  • Вкладка Entities - разрешения на операции с сущностями:

    При переходе на данную вкладку изначально включен флажок Assigned only, поэтому в таблице отображаются только сущности, для которых в данной роли уже есть явные разрешения. Поэтому для новой роли таблица пуста. Для установки разрешений снимите флажок Assigned only и нажмите Apply. Список сущностей можно фильтровать, вводя в поле Entity любую часть имени сущности и нажимая Apply.

    Установив флажок System level, можно выбрать системную сущность, помеченную аннотацией @SystemLevel. По умолчанию такие сущности не показываются в таблице.

  • Вкладка Attributes - разрешения на атрибуты сущностей:

    В таблице сущностей в колонке Permissions отображается список атрибутов, для которых явно указаны разрешения. Зеленым цветом обозначено разрешение modify (полный доступ), синим цветом - read-only (только чтение), красным - hide (атрибут скрыт).

    Управление списком сущностей аналогично описанному для вкладки Entities.

  • Вкладка Specific - разрешения на именованную функциональность:

    Имена объектов, на которые могут быть назначены специфические разрешения, определяются в конфигурационном файле permissions.xml проекта.

  • Вкладка UI - разрешения на UI-компоненты экранов:

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

    Для создания ограничения выберите нужный экран в выпадающем списке Screen, задайте путь к компоненту в поле Component, и нажмите Add. После этого установите режим доступа к выбранному компоненту в панели Permissions.

    Правила формирования пути к компоненту:

    • Если компонент принадлежит экрану, указывается просто идентификатор компонента id.

    • Если компонент принадлежит фрейму, вложенному в экран, то сначала указывается идентификатор фрейма, а затем через точку идентификатор компонента внутри фрейма.

    • Если необходимо установить разрешение для вкладки TabSheet или поля FieldGroup, то сначала указывается идентификатор компонента, а затем в квадратных скобках идентификатор соответственно вкладки или поля.

    • Чтобы установить разрешение на действие, необходимо указать идентификатор компонента, содержащего действие, а затем идентификатор действия в угловых скобках. Например: customersTable<changeGrade>.

7.1.6. Группы доступа

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

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

Управление группами доступа осуществляется в экране Administration -> Access Groups:

7.1.6.1. Ограничения

Ограничения (Constraints) дают возможность ограничить доступ к определенным экземплярам сущностей (записям таблиц).

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

Пользователь получает список ограничений от всех групп начиная со своей и вверх по иерархии. Тем самым реализуется принцип: чем ниже пользователь в иерархии групп, тем больше у него ограничений.

Для создания ограничения в экране Access Groups выберите группу, на которую нужно наложить ограничение, перейдите на вкладку Constraints и нажмите Create:

Далее выберите сущность в выпадающем списке Entity Name и задайте ограничение в полях Join Clause и Where Clause.

Редактор JPQL в полях Join Clause и Where Clause поддерживает автодополнение имен сущностей и их атрибутов. Для вызова автодополнения нажмите Ctrl+Space. Если вызов произведен после точки, будет выведен список атрибутов сущности, соответствующей контексту, иначе - список всех сущностей модели данных.

Правила формирования ограничения:

  • В качестве алиаса извлекаемой сущности необходимо использовать строку {E}. При выполнении запросов она будет заменена на реальный алиас, заданный в запросе.

  • В параметрах JPQL можно использовать следующие предопределенные константы:

    • session$userLogin − имя учетной записи текущего пользователя (в случае замещения − имя учетной записи замещаемого пользователя).

    • session$userId − ID текущего пользователя (в случае замещения − ID замещаемого пользователя).

    • session$userGroupId − ID группы текущего пользователя (в случае замещения − ID группы замещаемого пользователя).

    • session$XYZ − произвольный атрибут текущей пользовательской сессии, где XYZ − имя атрибута.

  • Содержимое поля Where Clause добавляется в выражение where запроса по условию and (И). Само слово where писать не нужно, оно будет добавлено автоматически, даже если исходный запрос его не содержал.

  • Содержимое поля Join Clause добавляется в выражение from запроса. Оно должно начинаться с запятой или слов join или left join.

Простейший пример ограничения приведен на рисунке выше: пользователи с данным ограничением будут видеть только те экземпляры сущности library$BookPublication, которые они создали сами.

7.1.6.2. Атрибуты сессии

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

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

Для создания атрибута в экране Access Groups выберите группу, перейдите на вкладку Session Attributes и нажмите Create:

В данном экране необходимо задать уникальное имя атрибута, тип данных и значение.

Получить атрибут сессии в коде приложения можно следующим способом:

@Inject
private UserSessionSource userSessionSource;
...
Integer accessLevel = userSessionSource.getUserSession().getAttribute("accessLevel");

Использовать атрибут в ограничениях можно, указав его в параметре JPQL с префиксом session$:

{E}.accessLevel = :session$accessLevel

7.1.7. Интеграция с LDAP

Интеграция CUBA-приложения c LDAP позволяет решить две задачи:

  1. Хранить пароли пользователей и управлять ими централизованно в базе данных LDAP.

  2. Для пользователей компьютеров, входящих в домен Windows, выполнять логин в приложение без ввода имени и пароля (то есть организовывать Single Sign-On).

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

Взаимодействие CUBA-приложения с LDAP осуществляется через интерфейс CubaAuthProvider. Платформа содержит единственную реализацию данного интерфейса - LdapAuthProvider, предназначенную для решения первой задачи. Для расширенной интеграции с Active Directory и обеспечения Single Sign-On можно использовать библиотеку Jespa и соответствующую имплементацию CubaAuthProvider, которая описана в Раздел 7.1.7.2, «Настройка аутентификации с использованием Jespa». При необходимости можно также создать собственный класс имплементации CubaAuthProvider и использовать его, установив следующие свойства приложения:

cuba.web.useActiveDirectory = true
cuba.web.activeDirectoryAuthClass = com.company.sample.web.MyAuthProvider

7.1.7.1. Базовая интеграция с Active Directory

Класс LdapAuthProvider используется по умолчанию при включенном свойстве приложения cuba.web.useActiveDirectory. В этом случае для аутентификации пользователей используется библиотека Spring LDAP.

Для настройки интеграции используются следующие свойства приложения блока Web Client:

  • cuba.web.ldap.urls - URL сервера Active Directory

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

  • cuba.web.ldap.user - значение атрибута sAMAccountName пользователя, имеющего право на чтение информации из Active Directory

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

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

cuba.web.useActiveDirectory = true
cuba.web.ldap.urls = ldap://192.168.1.1:389
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.user = myuser
cuba.web.ldap.password = mypassword

7.1.7.2. Настройка аутентификации с использованием Jespa

Jespa − библиотека для Java, обеспечивающая расширенную интеграцию между службой каталогов Active Directory и Java-приложениями по протоколу NTLMv2. Подробно о библиотеке см. http://www.ioplex.com.

7.1.7.2.1. Подключение библиотеки

Загрузите библиотеку с сайта http://www.ioplex.com и разместите JAR в каком-либо репозитории, зарегистрированном в вашем скрипте сборки build.gradle. Это может быть mavenLocal() или репозиторий вашей организации.

В файле build.gradle в секции конфигурации модуля web добавьте зависимость:

configure(webModule) {
    ...
    dependencies {
        compile('com.company.thirdparty:jespa:1.1.17')
    ...    

Создайте в модуле web класс реализации интерфейса CubaAuthProvider:

package com.company.sample.web;

import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.global.GlobalConfig;
import com.haulmont.cuba.core.global.Messages;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.web.auth.ActiveDirectoryHelper;
import com.haulmont.cuba.web.auth.CubaAuthProvider;
import com.haulmont.cuba.web.auth.DomainAliasesResolver;
import jespa.http.HttpSecurityService;
import jespa.ntlm.NtlmSecurityProvider;
import jespa.security.PasswordCredential;
import jespa.security.SecurityProviderException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class JespaAuthProvider extends HttpSecurityService implements CubaAuthProvider {

    private static class DomainInfo {
        private String bindStr;
        private String acctName;
        private String acctPassword;

        private DomainInfo(String bindStr, String acctName, String acctPassword) {
            this.acctName = acctName;
            this.acctPassword = acctPassword;
            this.bindStr = bindStr;
        }
    }

    private static Map<String, DomainInfo> domains = new HashMap<>();

    private static String defaultDomain;

    private Log log = LogFactory.getLog(getClass());

    @Inject
    private Configuration configuration;

    @Inject
    private Messages messages;

    @SuppressWarnings("deprecation")
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        initDomains();

        Map<String, String> properties = new HashMap<>();

        properties.put("jespa.bindstr", getBindStr());
        properties.put("jespa.service.acctname", getAcctName());
        properties.put("jespa.service.password", getAcctPassword());
        properties.put("jespa.account.canonicalForm", "3");
        properties.put("jespa.log.path", configuration.getConfig(GlobalConfig.class).getLogDir() + "/jespa.log");
        properties.put("http.parameter.anonymous.name", "anon");

        fillFromSystemProperties(properties);

        try {
            super.init(properties);
        } catch (SecurityProviderException e) {
            throw new ServletException(e);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        if (httpServletRequest.getHeader("User-Agent") != null) {
            String ua = httpServletRequest.getHeader("User-Agent").toLowerCase();
            boolean windows = ua.contains("windows");
            boolean gecko = ua.contains("gecko") && !ua.contains("webkit");
            if (!windows && gecko) {
                chain.doFilter(request, response);
                return;
            }
        }
        super.doFilter(request, response, chain);
    }

    @Override
    public void authenticate(String login, String password, Locale loc) throws LoginException {
        DomainAliasesResolver aliasesResolver = AppBeans.get(DomainAliasesResolver.NAME);

        String domain;
        String userName;

        int atSignPos = login.indexOf("@");
        if (atSignPos >= 0) {
            String domainAlias = login.substring(atSignPos + 1);
            domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
        } else {
            int slashPos = login.indexOf('\\');
            if (slashPos <= 0) {
                throw new LoginException(
                        messages.getMessage(ActiveDirectoryHelper.class, "activeDirectory.invalidName", loc),
                        login
                );
            }
            String domainAlias = login.substring(0, slashPos);
            domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
        }

        userName = login;

        DomainInfo domainInfo = domains.get(domain);
        if (domainInfo == null) {
            throw new LoginException(
                    messages.getMessage(ActiveDirectoryHelper.class, "activeDirectory.unknownDomain", loc),
                    domain
            );
        }

        Map<String, String> params = new HashMap<>();
        params.put("bindstr", domainInfo.bindStr);
        params.put("service.acctname", domainInfo.acctName);
        params.put("service.password", domainInfo.acctPassword);
        params.put("account.canonicalForm", "3");
        fillFromSystemProperties(params);

        NtlmSecurityProvider provider = new NtlmSecurityProvider(params);
        try {
            PasswordCredential credential = new PasswordCredential(userName, password.toCharArray());
            provider.authenticate(credential);
        } catch (SecurityProviderException e) {
            throw new LoginException(
                    messages.getMessage(ActiveDirectoryHelper.class, "activeDirectory.authenticationError", loc),
                    e.getMessage()
            );
        }
    }

    private void initDomains() {
        String domainsStr = AppContext.getProperty("cuba.web.activeDirectoryDomains");
        if (!StringUtils.isBlank(domainsStr)) {
            String[] strings = domainsStr.split(";");
            for (int i = 0; i < strings.length; i++) {
                String domain = strings[i];
                domain = domain.trim();
                if (!StringUtils.isBlank(domain)) {
                    String[] parts = domain.split("\\|");
                    if (parts.length != 4) {
                        log.error("Invalid ActiveDirectory domain definition: " + domain);
                        break;
                    } else {
                        domains.put(parts[0], new DomainInfo(parts[1], parts[2], parts[3]));
                        if (i == 0)
                            defaultDomain = parts[0];
                    }
                }
            }
        }
    }

    public String getDefaultDomain() {
        return defaultDomain != null ? defaultDomain : "";
    }

    public String getBindStr() {
        return getBindStr(getDefaultDomain());
    }

    public String getBindStr(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.bindStr : "";
    }

    public String getAcctName() {
        return getAcctName(getDefaultDomain());
    }

    public String getAcctName(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.acctName : "";
    }

    public String getAcctPassword() {
        return getAcctPassword(getDefaultDomain());
    }

    public String getAcctPassword(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.acctPassword : "";
    }

    public void fillFromSystemProperties(Map<String, String> params) {
        for (String name : AppContext.getPropertyNames()) {
            if (name.startsWith("jespa.")) {
                params.put(name, AppContext.getProperty(name));
            }
        }
    }
}
7.1.7.2.2. Настройка конфигурации
  • Выполнить настройки, описанные в разделе Installation -> Step 1: Create the Computer Account for NETLOGON Communication руководства Jespa Operator's Manual, которое можно загрузить по адресу http://www.ioplex.com/support.html.

  • Задать параметры доменов в local.app.properties в свойстве приложения cuba.web.activeDirectoryDomains. Каждый описатель домена имеет формат domain_name|full_domain_name|service_account_name|service_account_password. Описатели доменов отделяются друг от друга точкой с запятой.

    Например:

    cuba.web.activeDirectoryDomains = MYCOMPANY|mycompany.com|JESPA$@MYCOMPANY.COM|password1;TEST|test.com|JESPA$@TEST.COM|password2
  • Разрешить интеграцию с Active Directory, установив в local.app.properties свойство приложения cuba.web.useActiveDirectory:

    cuba.web.useActiveDirectory = true
  • Задать в local.app.properties дополнительные свойства для библиотеки (см. Jespa Operator's Manual). Например:

    jespa.log.level=3
  • Добавить адрес сервера в местную интрасеть в настройках браузера:

    • Для Internet Explorer и Chrome: Свойства обозревателя -> Безопасность -> Местная интрасеть -> Узлы -> Дополнительно

    • Для Firefox: about:config -> network.automatic-ntlm-auth.trusted-uris=http://myapp.mycompany.com

7.2. Примеры управления доступом

В данном разделе приведены практические рекомендации по настройке доступа пользователей к данным.

7.2.1. Настройка ролей

Рекомендованный способ настройки ролей и разрешений:

  1. Создать роль Default, отбирающую все права в системе. Проще всего это сделать, установив тип роли Denying. Включить флажок Default role, чтобы эта роль автоматически назначалась всем новым пользователям.

  2. Создать набор ролей, дающих нужные права различным категориям пользователей. Можно предложить две стратегии создания таких ролей:

    • Крупнозернистые (coarse-grained) роли - каждая роль содержит набор разрешений для всего круга обязанностей пользователя в системе. Например Sales Manager, Accountant. В этом случае пользователям в дополнение к запрещающей Default роли необходимо назначить как правило только одну разрешающую роль.

    • Мелкозернистые (fine-grained) роли - каждая роль содержит небольшой набор разрешений для выполнения пользователем некоторой функции в системе. Например Task Creator, References Editor. В этом случае пользователям в дополнение к запрещающей Default роли необходимо назначить несколько разрешающих ролей в соответствии с кругом их обязанностей.

    Разумеется, ничто не мешает совмещать обе стратегии.

  3. Администратору системы можно просто не назначать никаких ролей вообще, тогда у него будут все права на все объекты системы. Пользователя с запрещающими ролями можно сделать администратором, добавив ему роль типа Super.

7.2.2. Создание локальных администраторов

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

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

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

Рассмотрим следующую структуру групп доступа:

Задача:

  • Пользователи внутри группы Departments должны видеть только пользователей своей группы и ниже.

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

Способ решения задачи:

  • Задать для группы Departments следующие ограничения:

    • Для сущности sec$Group:

      {E}.id in (
        select h.group.id from sec$GroupHierarchy h
        where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
      )

      Это ограничение не позволяет пользователям видеть группы выше своей собственной.

    • Для сущности sec$User:

      {E}.group.id in (
        select h.group.id from sec$GroupHierarchy h
        where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
      )

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

    • Для сущности sec$Role:

      ({E}.description is null or {E}.description not like '[hide]')

      Данное ограничение не позволяет пользователям видеть роли, в атрибуте description которых записана строка [hide].

  • Создать роль, которая запретит редактирование ролей и разрешений:

    • Установите флажок Default role.

    • В поле Description добавьте строку [hide].

    • На вкладке Entities запретите операции create, update, delete для сущностей sec$Role и sec$Permission (для добавления разрешений на объект sec$Permission установите флажок System level).

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

Приложение A. Конфигурационные файлы

В данном приложении описаны основные конфигурационные файлы, входящие в состав CUBA-приложений.

A.1. context.xml

Файл context.xml является дескриптором развертывания приложения на сервере Apache Tomcat. В развернутом приложении этот файл располагается в подкаталоге META-INF каталога веб-приложения или WAR-файла, например, tomcat/webapps/app-core/META-INF/context.xml. В проекте файлы данного типа находятся в каталогах /web/META-INF модулей core, web, portal.

Основное предназначение файла для блока Middleware - определить источник данных и поместить его в JNDI под именем, заданным свойством приложения cuba.dataSourceJndiName .

Пример определения источника данных для PostgreSQL:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxActive="100"
  maxIdle="2"
  maxWait="5000"
  driverClassName="org.postgresql.Driver"
  username="cuba"
  password="cuba"
  url="jdbc:postgresql://localhost/sales"/>

Пример определения источника данных для Microsoft SQL Server:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxActive="100"
  maxIdle="2"
  maxWait="5000"
  driverClassName="net.sourceforge.jtds.jdbc.Driver"
  username="sa"
  password="saPass1"
  url="jdbc:jtds:sqlserver://localhost/sales"/>

Пример определения источника данных для Oracle:

<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxActive="100"
maxIdle="2"
maxWait="5000"
driverClassName="oracle.jdbc.OracleDriver"
username="sales"
password="sales"
url="jdbc:oracle:thin:@//localhost:1521/orcl"/>

Для всех блоков, являющихся веб-приложениями, данный файл может содержать код, отключающий сериализацию HTTP-сессий:

<Manager className="org.apache.catalina.session.PersistentManager" debug="0" distributable="false"
       saveOnRestart="false">
  <Store className="org.apache.catalina.session.FileStore"/>
</Manager>

A.2. datatypes.xml

Файл datatypes.xml определяет доступные типы данных атрибутов сущностей, см. Раздел 4.2.2.3, «Datatype»

Файл по умолчанию расположен в пакете com.haulmont.chile.core.datatypes базового проекта cuba. Если в прикладном проекте в модуле global создать аналогичный файл в корне CLASSPATH, то типы данных будут загружены из него.

Механизм загрузки не поддерживает расширение, т.е. все типы данных загружаются из одного файла - либо из корня CLASSPATH, либо из пакета com.haulmont.chile.core.datatypes.

Доступные типы данных описываются в элементах datatype. Обязательным атрибутом является только class, в котором указывается класс типа данных, реализующий интерфейс Datatype. Набор остальных атрибутов зависит от класса, объекту которого при создании передается соответствующий XML элемент, и разбор атрибутов происходит в этом классе.

Типичные атрибуты:

  • format - формат преобразования в строку без учета локали

  • groupingSeparator - символ-разделитель групп разрядов числа при форматировании без учета локали

  • decimalSeparator - символ-разделитель целой и дробной части числа при форматировании без учета локали

Пример:

<datatypes>

  <datatype class="com.haulmont.chile.core.datatypes.impl.BooleanDatatype"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.IntegerDatatype"
            format="0" groupingSeparator=""/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.LongDatatype"
            format="0" groupingSeparator=""/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.DoubleDatatype"
            format="0.###" decimalSeparator="." groupingSeparator=""/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype"
            format="0.####" decimalSeparator="." groupingSeparator=""/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.StringDatatype"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.DateTimeDatatype"
            format="yyyy-MM-dd'T'HH:mm:ss.SSS"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.DateDatatype"
            format="yyyy-MM-dd"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.TimeDatatype"
            format="HH:mm:ss"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.UUIDDatatype"/>

  <datatype class="com.haulmont.chile.core.datatypes.impl.ByteArrayDatatype"/>

</datatypes>

A.3. dispatcher-spring.xml

Файлы данного типа определяют конфигурацию дополнительного контейнера Spring Framework для клиентских блоков, содержащих контроллеры Spring MVC.

Набор файлов dispatcher-spring.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.dispatcherSpringContextConfig . Последовательность перечисления файлов важна, так как каждый последующий файл может переопределять конфигурацию бинов, заданную в предыдущих файлах.

Контейнер контроллеров Spring MVC создается таким образом, что основной контейнер (конфигурируемый файлами spring.xml ) является родительским по отношению к нему. Это означает, что бины контейнера контроллеров могут обращаться к бинам основного контейнера, а бины основного контейнера "не видят" контейнер контроллеров.

A.4. menu.xml

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

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/menu.xsd

Набор файлов menu.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.menuConfig .

Рассмотрим структуру файла.

menu-config - корневой элемент

Элементы menu-config, образующие древовидную структуру:

  • menu - раскрывающееся меню, содержащее пункты и другие раскрывающиеся меню

    Атрибуты menu:

    • id - идентификатор элемента, использующийся для формирования локализованного названия (см. ниже)

    • insertBefore, insertAfter - идентификатор элемента или пункта меню, перед которым или после которого нужно вставить данный элемент. Используется в прикладном проекте для вставки элемента в нужное место меню, определенного в аналогичных файлах базовых проектов. Разумеется, использование одного из этих атрибутов для конкретного элемента исключает возможность использования второго атрибута для данного элемента.

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

    Элементы menu:

    • menu

    • item - пункт меню, см. далее

    • separator - разделитель

  • item - пункт меню

    Атрибуты item:

    • id - идентификатор элемента, использующийся для формирования локализованного названия (см. ниже), и для связи с элементом файла screens.xml , в котором зарегистрированы экраны UI. При выборе пункта меню в главном окне приложения будет открыт соответствующий экран.

    • shortcut - горячая клавиша для вызова данного пункта меню. Возможные модификаторы - ALT, CTRL, SHIFT - отделяются символом "-". Например:

      shortcut="ALT-C"
      shortcut="ALT-CTRL-C"
      shortcut="ALT-CTRL-SHIFT-C"

      Горячие клавиши можно также задавать в свойствах приложения и использовать в menu.xml следующим образом:

      shortcut="${sales.menu.customer}"
    • openType - тип открытия экрана, возможные значения соответствуют перечислению WindowManager.OpenType: NEW_TAB, THIS_TAB, DIALOG, NEW_WINDOW.

      По умолчанию - NEW_TAB.

      Значение NEW_WINDOW поддерживается только в Desktop Client, в Web Client оно эквивалентно NEW_TAB.

    • insertBefore, insertAfter - идентификатор элемента или пункта меню, перед которым или после которого нужно вставить данный элемент.

      Атрибуты insertBefore, insertAfter для элемента item не поддерживаются в Studio. Поэтому если вы задали эти атрибуты вручную, не открывайте дизайнер меню Studio, иначе они будут удалены.

    • resizable - актуально для типа открытия экрана DIALOG - задает окну возможность изменения размера. Возможные значения: true, false.

      По умолчанию главное меню не влияет на возможность изменения размера диалоговых окон.

    Элементы item:

    • param - задает параметр экрана, передаваемый в мэп метода init() контроллера. Параметры, заданные в menu.xml, переопределяют одноименные параметры, заданные в screens.xml .

      Атрибуты param:

      • name - имя параметра

      • value - значение параметра. Строковое значение может преобразовываться в некоторый объект по следующим правилам:

        • Если строка представляет собой идентификатор сущности, записанный по правилам класса EntityLoadInfo, то загружается указанный экземпляр сущности.

        • Если строка имеет вид ${some_name}, то значением параметра будет свойство приложения some_name.

        • Строки true и false преобразуются в соответствующие значения типа Boolean.

        • Если ничего из вышеперечисленного не подходит, значением параметра становится сама строка.

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

      Данный элемент должен содержать вложенные элементы permission, каждый из которых описывает одно требуемое разрешение. Пункт меню доступен только при наличии всех требуемых разрешений.

      Атрибуты permission:

      • type - тип требуемого разрешения, задаваемый значением перечисления PermissionType: SCREEN, ENTITY_OP, ENTITY_ATTR, SPECIFIC, UI.

      • target - объект, на который проверяется наличие разрешения. Зависит от типа разрешения:

        • SCREEN - идентификатор экрана, например sales$Customer.lookup.

        • ENTITY_OP - строка вида {entity_name}:{op}, где {op} - read, create, update, delete. Например: sales$Customer:create.

        • ENTITY_ATTR - строка вида {entity_name}:{attribute}, например sales$Customer:name.

        • SPECIFIC - идентификатор специфического разрешения, например sales.runInvoicing.

        • UI - путь к визуальному компоненту экрана.

Пример файла меню:

<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">

  <menu id="sales" insertBefore="administration">
      <item id="sales$Customer.lookup"/>
      <separator/>
      <item id="sales$Order.lookup"/>
  </menu>

</menu-config>

Локализованное название элемента меню формируется следующим образом: к идентификатору элемента добавляется префикс menu-config с точкой на конце, и полученная строка используется как ключ в главном пакете сообщений. Например:

menu-config.sales=Sales
menu-config.sales$Customer.lookup=Customers

A.5. metadata.xml

Файлы данного типа используются для регистрации неперсистентных сущностей и присвоения мета-аннотаций, см. Раздел 4.2.2, «Metadata Framework»

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/metadata.xsd

Набор файлов metadata.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.metadataConfig .

Рассмотрим структуру файла.

metadata - корневой элемент.

Элементы metadata:

  • metadata-model - описатель метамодели проекта.

    Атрибут metadata-model :

    • root-package - корневой пакет проекта.

    Элементы metadata-model:

    • class - класс неперсистентной сущности.

  • annotations - корень элементов присвоения мета-аннотаций сущностей.

    Элементы annotations:

    • entity - элемент сущности, которой присваиваются мета-аннотации.

      Атрибуты entity:

      • class - класс сущности.

      Элементы entity:

      • annotation - элемент мета-аннотации.

        Атрибуты annotation:

        • name - имя мета-аннотации.

        • value - значение мета-аннотации.

Пример:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">

  <metadata-model root-package="com.sample.sales">
      <class>com.sample.sales.entity.SomeTransientEntity</class>
      <class>com.sample.sales.entity.OtherTransientEntity</class>
  </metadata-model>

  <annotations>
      <entity class="com.haulmont.cuba.security.entity.User">
          <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory"
                      value="true"/>
          <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore"
                      value="true"/>
      </entity>
  </annotations>

</metadata>

A.6. permissions.xml

Файлы данного типа используются в блоках Web Client и Desktop Client для регистрации специфических разрешений пользователей.

Набор файлов permissions.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.permissionConfig .

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/permissions.xsd.

Рассмотрим структуру файла.

permission-config - корневой элемент.

Элементы permission-config:

  • specific - описатель специфических разрешений.

    Элементы specific:

    • category - категория разрешений, используется для группировки в экране управления разрешениями роли. Атрибут id используется как ключ для получения локализованного названия категории.

    • permission - именованное разрешение. Атрибут id используется для получения значения разрешения методом Security.isSpecificPermitted(), а также как ключ для получения локализованного названия разрешения для отображения в экране управления разрешениями роли.

Пример:

<permission-config xmlns="http://schemas.haulmont.com/cuba/permissions.xsd">
  <specific>
      <category id="app">
          <permission id="app.doSomething"/>
          <permission id="app.doSomethingOther"/>
      </category>
  </specific>
</permission-config>

A.7. persistence.xml

Файлы данного типа являются стандартными для JPA и используются для регистрации персистентных сущностей и задания параметров функционирования фреймворка ORM.

Набор файлов persistence.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.persistenceConfig .

На старте блока Middleware из заданных файлов собирается один persistence.xml и сохраняется в рабочем каталоге приложения. Параметры ORM могут переопределяться каждым следующим файлом списка, поэтому порядок указания файлов важен. Существует несколько параметров, жестко определяемых типом используемой СУБД (заданным в cuba.dbmsType ), которые невозможно переопределить в persistence.xml, это:

  • openjpa.jdbc.DBDictionary

  • openjpa.jdbc.MappingDefaults

Кроме того, при выключенном свойстве приложения cuba.disableOrmXmlGeneration и наличии расширенных сущностей на старте приложения в рабочем каталоге создается файл orm.xml и путь к нему записывается в параметр openjpa.MetaDataFactory. В этом случае параметр openjpa.MetaDataFactory также нельзя задать в persistence.xml заранее.

Пример файла:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
  <persistence-unit name="sales" transaction-type="RESOURCE_LOCAL">
      <class>com.sample.sales.entity.Customer</class>
      <class>com.sample.sales.entity.Order</class>
  </persistence-unit>
</persistence>

A.8. remoting-spring.xml

Файлы данного типа определяют конфигурацию дополнительного контейнера Spring Framework для блока Middleware, который предназначен для экспорта сервисов и других компонентов среднего слоя, доступных клиентскому уровню (далее контейнер удаленного доступа).

Набор файлов remoting-spring.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.remotingSpringContextConfig . Последовательность перечисления файлов важна, так как каждый последующий файл может переопределять конфигурацию бинов, заданную в предыдущих файлах.

Контейнер удаленного доступа создается таким образом, что основной контейнер (конфигурируемый файлами spring.xml ) является родительским по отношению к нему. Это означает, что бины контейнера удаленного доступа могут обращаться к бинам основного контейнера, а бины основного контейнера "не видят" контейнер удаленного доступа.

Основная задача контейнера удаленного доступа - сделать сервисы Middleware доступными клиентскому уровню с помощью механизма Spring HttpInvoker. Для этого в cuba-remoting-spring.xml базового проекта cuba определяется бин servicesExporter типа RemoteServicesBeanCreator, который получает из основного контейнера все классы сервисов и экспортирует их. В дополнение к обычным аннотированным сервисам контейнер удаленного доступа экспортирует некоторые специфические бины, такие как LoginService.

Кроме того, cuba-remoting-spring.xml определяет базовый пакет, начиная с которого производится поиск аннотированных классов контроллеров Spring MVC, используемых для загрузки-выгрузки файлов.

В прикладном проекте определять файл типа remoting-spring.xml необходимо только в том случае, если создаются специфические контроллеры Spring MVC. Сервисы прикладного проекта в любом случае будут импортированы стандартным бином servicesExporter, определенным в базовом проекте cuba.

A.9. screens.xml

Файлы данного типа используются в блоках Web Client и Desktop Client, реализующих универсальный пользовательский интерфейс, для регистрации XML-дескрипторов экранов.

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/screens.xsd

Набор файлов screens.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.windowConfig .

Рассмотрим структуру файла.

screen-config - корневой элемент

Элементы screen-config:

  • screen - описатель экрана

    Атрибуты screen:

    • id - идентификатор экрана, по которому он доступен в программном коде (например, в методах IFrame.openWindow() и т.п.) и в menu.xml .

    • template - путь к файлу XML-дескриптора экрана. Загрузка производится по правилам интерфейса Resources .

    • class - если атрибут template не указан, в данном атрибуте нужно указать имя класса, реализующего либо Callable, либо Runnable.

      В случае Callable метод call() должен возвращать экземпляр созданного Window, который будет возвращен вызывающему коду как результат WindowManager.openWindow(). Класс может содержать конструктор с параметрами для передачи ему строковых значений, заданных вложенным элементом param (см. ниже).

    • multipleOpen - опциональный атрибут, задающий возможность многократного открытия экрана. Если равен false или не задан, и в главном окне уже открыт экран с данным идентификатором, то вместо открытия нового экземпляра экрана отобразится имеющийся. Значение true позволяет открывать произвольное количество одинаковых экранов.

    Элементы screen:

    • param - задает параметр экрана, передаваемый в мэп метода init() контроллера. Параметры, передаваемые из вызывающего кода в методы openWindow(), переопределяют одноименные параметры, заданные в screens.xml.

      Атрибуты param:

      • name - имя параметра

      • value - значение параметра. Строки true и false автоматически преобразуются в значения типа Boolean.

  • include - включение другого файла типа screens.xml

    Атрибуты include:

    • file - путь к файлу по правилам интерфейса Resources

Пример файла screens.xml:

<screen-config xmlns="http://schemas.haulmont.com/cuba/screens.xsd">

  <screen id="sales$Customer.lookup" template="/com/sample/sales/gui/customer/customer-browse.xml"/>
  <screen id="sales$Customer.edit" template="/com/sample/sales/gui/customer/customer-edit.xml"/>

  <screen id="sales$Order.lookup" template="/com/sample/sales/gui/order/order-browse.xml"/>
  <screen id="sales$Order.edit" template="/com/sample/sales/gui/order/order-edit.xml"/>

</screen-config>

A.10. spring.xml

Файлы данного типа определяют конфигурацию основного контейнера Spring Framework для каждого блока приложения.

Набор файлов spring.xml, включая определенные в базовых проектах, задается в свойстве приложения cuba.springContextConfig . Последовательность перечисления файлов важна, так как каждый последующий файл может переопределять конфигурацию бинов, заданную в предыдущих файлах.

Основная часть конфигурирования контейнера возложена на аннотации бинов (такие как @ManagedBean, @Service, @Inject и др.), поэтому обязательной частью spring.xml в прикладном проекте является только элемент context:component-scan, в котором задается базовый пакет Java, с которого начинается поиск аннотированных классов. Например:

<context:component-scan base-package="com.sample.sales"/>

Остальное содержимое зависит от того, для какого блока приложения конфигурируется контейнер: например, для Middleware это регистрация JMX-бинов, для блоков клиентского уровня - импорт сервисов.

A.11. views.xml

Файлы данного типа используются для описания представлений, см. Раздел 4.2.3, «Представления»

Схема XML доступна по адресу http://schemas.haulmont.com/cuba/5.6/view.xsd

views - корневой элемент

Элементы views:

  • view - описатель View

    Атрибуты view:

    • class - класс сущности.

    • entity - имя сущности, например sales$Order. Может быть использован вместо атрибута class.

    • name - имя представления, должно быть уникальным в пределах сущности.

    • systemProperties - признак включения системных атрибутов сущности (входящих в состав базовых интерфейсов персистентных сущностей BaseEntity и Updatable). Необязательный атрибут, по умолчанию false.

    • overwrite - признак того, что данный описатель должен переопределить представление с таким же классом и именем, уже развернутое в репозитории. Необязательный атрибут, по умолчанию false.

    • extends - указывает имя представления той же сущности, от которого нужно унаследовать атрибуты. Порядок следования описателей в файле при этом не важен. Например, при указании extends="_local" в текущее представление будут включены все локальные атрибуты сущности. Необязательный атрибут.

    Элементы view:

    • property - описатель ViewProperty.

      Атрибуты property:

      • name - имя атрибута сущности.

      • view - для ссылочного атрибута указывает имя представления, с которым должна загружаться ассоциированная сущность. Порядок следования описателей в файле при этом не важен.

      • lazy - для ссылочных атрибутов признак того, что данный атрибут нужно не включать в Fetch Plan, а загружать отдельным SQL запросом, инициированным обращением к атрибуту. Необязательный атрибут, по умолчанию false.

        Рекомендуется использовать lazy для атрибутов-коллекций, если таких атрибутов больше одного для данного графа представлений. Т.е. устанавливайте lazy = "true" для всех коллекций, кроме одной.

      Элементы property:

      • property - описатель атрибута связанной сущности. Таким способом можно определить неименованное представление для связанной сущности прямо внутри текущего описателя (inline).

  • include - включение другого файла типа views.xml

    Атрибуты include:

    • file - путь к файлу по правилам интерфейса Resources

Пример:

<views xmlns="http://schemas.haulmont.com/cuba/view.xsd">

  <view class="com.sample.sales.entity.Order"
        name="orderWithCustomer"
        extends="_local">
      <property name="customer" view="_minimal"/>
  </view>

  <view class="com.sample.sales.entity.Item"
        name="itemsInOrder">
      <property name="quantity"/>
      <property name="product" view="_minimal"/>
  </view>

  <view class="com.sample.sales.entity.Order"
        name="orderWithCustomerDefinedInline"
        extends="_local">
      <property name="customer">
          <property name="name"/>
          <property name="email"/>
      </property>
  </view>

</views>

См. также свойство приложения cuba.viewsConfig

A.12. web.xml

Файл web.xml является стандартным дескриптором веб-приложения Java EE, и должен быть создан для блоков Middleware, Web Client и Web Portal.

В проекте приложения файлы web.xml располагаются в каталогах web/WEB-INF соответствующих модулей.

Рассмотрим содержимое web.xml блока Middleware (модуль core проекта):

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

  <!-- Application properties config files -->
  <context-param>
      <param-name>appPropertiesConfig</param-name>
      <param-value>
          classpath:cuba-app.properties
          classpath:app.properties
          file:${catalina.home}/conf/app-core/local.app.properties
      </param-value>
  </context-param>

  <listener>
      <listener-class>com.haulmont.cuba.core.sys.AppContextLoader</listener-class>
  </listener>

  <servlet>
      <servlet-name>remoting</servlet-name>
      <servlet-class>com.haulmont.cuba.core.sys.remoting.RemotingServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
      <servlet-name>remoting</servlet-name>
      <url-pattern>/remoting/*</url-pattern>
  </servlet-mapping>

  <servlet>
      <servlet-name>restapi</servlet-name>
      <servlet-class>com.haulmont.cuba.core.sys.restapi.RestApiServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
      <servlet-name>restapi</servlet-name>
      <url-pattern>/api/*</url-pattern>
  </servlet-mapping>
</web-app>

В элементах context-param задаются инициализирующие параметры объекта ServletContext данного веб-приложения. Здесь определен параметр appPropertiesConfig, значением которого является список файлов свойств приложения.

В элементе listener задается класс слушателя, реализующего интерфейс ServletContextListener. В блоке Middleware CUBA-приложения в качестве слушателя должен использоваться класс AppContextLoader, выполняющий инициализацию AppContext .

Далее следуют определения сервлетов, среди которых обязательным для Middleware является класс RemotingServlet, связанный с контейнером удаленного доступа (см. Раздел A.8, «remoting-spring.xml»). Данный сервлет отображен на URL /remoting/*.

Рассмотрим содержимое web.xml блока Web Client (модуль web проекта):

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

  <context-param>
      <description>Vaadin production mode</description>
      <param-name>productionMode</param-name>
      <param-value>false</param-value>
  </context-param>

  <context-param>
      <param-name>appPropertiesConfig</param-name>
      <param-value>
          classpath:cuba-web-app.properties
          classpath:web-app.properties
          file:${catalina.home}/conf/app/local.app.properties
      </param-value>
  </context-param>

  <listener>
      <listener-class>com.haulmont.cuba.web.sys.WebAppContextLoader</listener-class>
  </listener>

  <servlet>
      <servlet-name>app_servlet</servlet-name>
      <servlet-class>com.haulmont.cuba.web.sys.CubaApplicationServlet</servlet-class>
      <init-param>
          <param-name>application</param-name>
          <param-value>com.haulmont.sales.web.App</param-value>
      </init-param>
      <init-param>
          <param-name>widgetset</param-name>
          <param-value>com.haulmont.cuba.web.toolkit.ui.WidgetSet</param-value>
      </init-param>
      <init-param>
          <param-name>UI</param-name>
          <param-value>com.haulmont.cuba.web.AppUI</param-value>
      </init-param>
      <init-param>
          <param-name>UIProvider</param-name>
          <param-value>com.haulmont.cuba.web.sys.CubaUIProvider</param-value>
      </init-param>
  </servlet>

  <servlet-mapping>
      <servlet-name>app_servlet</servlet-name>
      <url-pattern>/*</url-pattern>
  </servlet-mapping>

  <filter>
      <filter-name>cuba_filter</filter-name>
      <filter-class>com.haulmont.cuba.web.sys.CubaHttpFilter</filter-class>
  </filter>

  <filter-mapping>
      <filter-name>cuba_filter</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

В данном файле определены два параметра: appPropertiesConfig, значением которого является список файлов свойств приложения, и productionMode, отключающий режим отладки фреймворка Vaadin.

В качестве ServletContextListener в блоке Web Client используется класс WebAppContextLoader.

Далее определяется сервлет CubaApplicationServlet, входящий в состав основанной на фреймворке Vaadin реализации универсального пользовательского интерфейса. Сервлет имеет несколько параметров, в том числе:

  • application - задает специфический для проекта класс клиентского приложения, который должен быть наследником com.haulmont.cuba.web.App

  • widgetset - задает набор GWT компонентов, используемых на стороне веб-браузера

После определения сервлетов подключается фильтр CubaHttpFilter, необходимый для функционирования блока Web Client.

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

В данном приложении в алфавитном порядке описаны доступные свойства приложения.

cuba.allowQueryFromSelected

Разрешает универсальному фильтру использовать режим последовательного наложения фильтров. См. также Раздел 4.2.6.10.2, «Последовательная выборка»

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

Интерфейс: GlobalConfig

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

cuba.allowSetNotLoadedAttributes

Разрешает вызывать setter для незагруженных атрибутов Detached сущностей. Подробнее см. Раздел 4.2.3, «Представления»

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

Используется во всех блоках приложения.

cuba.automaticDatabaseUpdate

Включает режим выполнения скриптов БД сервером на старте приложения.

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

Интерфейс: ServerConfig

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

cuba.availableLocales

Список поддерживаемых языков интерфейса.

Формат свойства: {название_языка1}|{код_языка_1};{название_языка2}|{код_языка_2};... Пример:

cuba.availableLocales=French|fr;English|en

{название_языка} − это название, которое будет отображаться в списках доступных языков. Например, в окне входа в систему, в экране редактирования пользователя.

{код_языка} − соответствует коду, возвращаемому методом Locale.getLanguage(). Используется как суффикс для формирования имен файлов пакетов сообщений. Например, messages_fr.properties.

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

Значение по умолчанию: English|en;Russian|ru;French|fr

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.backgroundWorker.maxActiveTasksCount

Максимальное количество активных фоновых задач.

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

Интерфейс: WebConfig

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

cuba.backgroundWorker.maxClientLatencySeconds

Задержка в секундах, которая добавляется к таймауту фоновой задачи, прежде чем она будет прервана механизмом WatchDog. Отражает возможные сетевые задержки при опросе статуса задачи.

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

Интерфейс: WebConfig

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

cuba.backgroundWorker.uiCheckInterval

Период опроса состояния фоновых задач.

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

Интерфейс: WebConfig

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

cuba.client.maxUploadSizeMb

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

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

Интерфейс: ClientConfig

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

cuba.cluster.enabled

Включает взаимодействие серверов Middleware в кластере. Подробнее см. Раздел 6.3.2.2, «Настройка взаимодействия серверов Middleware».

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

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

cuba.confDir

Конфигурационный параметр, задающий расположение каталога конфигурации данного блока приложения.

Значение по умолчанию для блоков Middleware, Web Client, Web Portal: ${catalina.home}/conf/${cuba.webContextName}, что в случае стандартного развертывания в Tomcat означает расположение внутри каталога tomcat/conf в подкаталоге с именем текущего веб-приложения, например, tomcat/conf/app-core.

Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/conf.

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.connectionReadTimeout

Задает таймаут подключения клиентского блока к Middleware. Неотрицательное значение передается в метод setReadTimeout() класса URLConnection.

См. также cuba.connectionTimeout.

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

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

cuba.connectionTimeout

Задает таймаут подключения клиентского блока к Middleware. Неотрицательное значение передается в метод setConnectTimeout() класса URLConnection.

См. также cuba.connectionReadTimeout.

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

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

cuba.connectionUrlList

Задает список URL для подключения клиентских блоков к серверам Middleware.

Значением свойства должен быть один или несколько разделенных запятой URL вида http[s]://host[:port]/app-core, где host - имя сервера, port - порт сервера, app-core - имя веб-приложения, реализующего блок Middleware. Например:

cuba.connectionUrlList=http://localhost:8080/app-core

В случае использования кластера серверов Middleware, для обеспечения отказоустойчивости и балансировки нагрузки необходимо перечислить их адреса через запятую:

cuba.connectionUrlList=http://server1:8080/app-core,http://server2:8080/app-core

При этом порядок серверов в данном списке определяет приоритет, в котором клиент будет пытаться направлять запросы. Например в данном случае клиент сначала попытается вызвать server1, если он недоступен - то server2. Если запрос к server2 завершился успешно, данный клиент ставит server2 первым в своем списке и продолжает работать с ним. После перезапуска клиента список восстанавливается в первоначальное значение. Для обеспечения равномерного распределения клиентов между серверами используется свойство cuba.randomServerPriority.

См. также свойство cuba.useLocalServiceInvocation.

Интерфейс: ClientConfig

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

cuba.creditsConfig

Конфигурационный параметр, задает набор файлов credits.xml, содержащих информацию об используемом программном обеспечении.

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

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

Пример:

cuba.creditsConfig=cuba-credits.xml reports-credits.xml credits.xml
cuba.dataSourceJndiName

Задает JNDI имя источника данных javax.sql.DataSource, через который производится обращение к базе данных приложения.

Значение по умолчанию: java:comp/env/jdbc/CubaDS

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

cuba.dataDir

Конфигурационный параметр, задающий расположение рабочего каталога данного блока приложения.

Значение по умолчанию для блоков Middleware, Web Client, Web Portal: ${catalina.home}/work/${cuba.webContextName}, что в случае стандартного развертывания в Tomcat означает расположение внутри каталога tomcat/work в подкаталоге с именем текущего веб-приложения, например, tomcat/work/app-core.

Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/work.

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.dbDir

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

Значение по умолчанию: ${catalina.home}/webapps/${cuba.webContextName}/WEB-INF/db, что означает расположение в подкаталоге WEB-INF/db каталога текущего веб-приложения Tomcat.

Интерфейс: ServerConfig

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

cuba.dbmsType

Задает тип используемой базы данных. Совместно с cuba.dbmsVersion влияет на выбор имплементаций интерфейсов интеграции с СУБД и на поиск скриптов создания и обновления БД.

Подробнее см. Раздел 4.3.1, «Типы СУБД»

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

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

cuba.dbmsVersion

Необязательное свойство, задающее версию используемой базы данных. Совместно с cuba.dbmsType влияет на выбор имплементаций интерфейсов интеграции с СУБД и на поиск скриптов создания и обновления БД.

Подробнее см. Раздел 4.3.1, «Типы СУБД»

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

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

cuba.defaultQueryTimeoutSec

Задает таймаут транзакции по умолчанию.

Значение по умолчанию: 0, означает, что таймаут отсутствует.

Интерфейс: ServerConfig

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

cuba.desktop.useServerTime

Включает корректировку времени, выдаваемого интерфейсом TimeSource блока DesktopClient - оно становится примерно равным времени Middleware, к которому подключен данный клиент.

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

Интерфейс: DesktopConfig

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

cuba.desktop.useServerTimeZone

Устанавливает в JVM блока DesktopClient timezone Middleware, к которому подключен данный клиент.

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

Интерфейс: DesktopConfig

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

cuba.disableOrmXmlGeneration

Запрещает автоматическую генерацию файла orm.xml для расширенных сущностей. Дает возможность вручную создать такой файл и зарегистрировать в параметре openjpa.MetaDataFactory файла persistence.xml .

Значение по умолчанию: false, означает что orm.xml будет создан автоматически при наличии расширенных сущностей.

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

cuba.dispatcherSpringContextConfig

Конфигурационный параметр, задает набор файлов dispatcher-spring.xml в клиентских блоках.

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

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

Пример:

cuba.dispatcherSpringContextConfig=cuba-portal-dispatcher-spring.xml portal-dispatcher-spring.xml
cuba.download.directories

Задает список каталогов, из которых можно загружать с Middleware файлы через com.haulmont.cuba.core.controllers.FileDownloadController. Загрузка файлов используется в частности механизмом отображения журналов сервера, доступным через экран Администрирование -> Журнал сервера веб-клиента.

Список задается через ";".

Значение по умолчанию: ${cuba.tempDir};${cuba.logDir}, означает что файлы можно загружать из временного каталога и каталога логов.

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

cuba.email.*

Параметры отправки email, подробно описаны в Раздел 4.7.2.3, «Настройка параметров отправки email»

cuba.fileStorageDir

Задает корни структуры каталогов файлового хранилища. Подробнее см. Раздел 4.7.8.3, «Стандартная реализация хранилища»

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

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

cuba.gui.genericFilterChecking

Оказывает влияние на поведение компонента Filter.

При установке в true пользователь не может применить фильтр, не введя ни одного параметра.

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

Интерфейс: ClientConfig

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

cuba.gui.genericFilterControlsLayout

Задает шаблон расположения элементов компонента Filter. Каждый элемент имеет следующий формат: [component_name | options-comma-separated], например [pin | no-caption, no-icon].

Доступные элементы:

  • filters_popup - кнопка с выпадающим списком фильтров, объединенная с кнопкой Search button.

  • filters_lookup - поле с выпадающим списком фильтров. При использовании этого элемента необходимо добавить также элемент search.

  • search - кнопка Search. Не добавляйте, если уже используется filters_popup.

  • add_condition - кнопка-ссылка для добавления новых условий.

  • spacer - пустое пространство между элементами.

  • settings - кнопка с выпадающим списком Settings. Элементы списка кнопки задаются в виде опций (см. ниже).

  • max_results - группа компонентов для задания максимального количества извлекаемых записей.

  • fts_switch - флажок для переключения в режим полнотекстового поиска.

Следующие действия могут быть опциями элемента settings: save, save_as, edit, remove, pin, make_default, save_search_folder, save_app_folder.

Они также могут быть использованы и как независимые элементы компоновки. В этом случае они могут иметь следующие опции:

  • no-icon - если кнопка действия не должна иметь значка. Например: [save | no-icon].

  • no-caption - если кнопка действия не должна иметь заголовка. Например: [pin | no-caption].

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

[filters_popup] [add_condition] [spacer] \
[settings | save, save_as, edit, remove, make_default, pin, save_search_folder, save_app_folder] \
[max_results] [fts_switch]

Интерфейс: ClientConfig

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

cuba.gui.genericFilterManualApplyRequired

Оказывает влияние на поведение компонента Filter.

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

При открытии экрана списка с помощью папки приложения или папки поиска значение cuba.gui.genericFilterManualApplyRequired не учитывается, то есть в этом случае фильтр будет применяться. Фильтр не применится, если значение атрибута applyDefault у папки явно установлено в false.

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

Интерфейс: ClientConfig

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

cuba.gui.layoutAnalyzerEnabled

Позволяет отключить команду анализа компоновки экрана Analyze layout, доступную в контекстном меню вкладок главного окна и в заголовках модальных окон.

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

Интерфейс: ClientConfig

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

cuba.gui.genericFilterColumnsQty

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

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

Интерфейс: ClientConfig

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

cuba.gui.genericFilterConditionsLocation

Определяет положение панели условий фильтра. Доступны два положения: top (над элементами управления фильтром) и bottom (под элементами управления фильтром).

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

Интерфейс: ClientConfig

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

cuba.gui.genericFilterPopupListSize

Определяет число элементов, отображающихся в выпадающем списке кнопки Search. Если количество фильтров превышает значение, к выпадающему списку добавляется действие Show more.... Действие открывает новое диалоговое окно со списком всех доступных фильтров.

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

Интерфейс: ClientConfig

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

cuba.groovyEvaluationPoolMaxIdle

Задает максимальное число неиспользуемых скомпилированных выражений Groovy в пуле при выполнении метода Scripting.evaluateGroovy(). Данный параметр рекомендуется увеличивать при потребности в интенсивном исполнении выражений Groovy, например, вследствие большого количества папок приложения.

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

Используется во всех стандартных блоках.

cuba.groovyEvaluatorImport

Задает список классов, импортируемых всеми выполняемыми через Scripting выражениями на Groovy.

Имена классов в списке разделяются запятой или точкой с запятой.

Значение по умолчанию: com.haulmont.cuba.core.global.PersistenceHelper

Используется во всех стандартных блоках.

Пример:

cuba.groovyEvaluatorImport=com.haulmont.cuba.core.global.PersistenceHelper,com.abc.sales.CommonUtils
cuba.gui.useSaveConfirmation

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

Значение true задает форму с тремя вариантами выбора: сохранить изменения, не сохранять, либо не закрывать экран.

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

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

Интерфейс: ClientConfig

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

cuba.httpSessionExpirationTimeoutSec

Задает таймаут бездействия HTTP-сессии в секундах.

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

Интерфейс: WebConfig

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

Рекомендуется выставлять параметры cuba.userSessionExpirationTimeoutSec и cuba.httpSessionExpirationTimeoutSec в одинаковое значение.

cuba.inMemoryDistinct

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

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

Интерфейс: ServerConfig

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

cuba.jmxUserLogin

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

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

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

cuba.licensePath

Путь к файлу лицензии на использование платформы CUBA. Файл загружается по правилам интерфейса Resources. См. также Раздел 6.6, «Использование файла лицензии».

Значение по умолчанию: /cuba.license

Интерфейс: ServerConfig

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

cuba.localeSelectVisible

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

Если cuba.localeSelectVisible=false, то локаль пользовательской сессии выбирается следующим образом:

  • если для данного экземпляра сущности User установлен атрибут language, то устанавливается локаль для этого языка;

  • если язык операционной системы пользователя присутствует в списке доступных (заданных свойством cuba.availableLocales ), то выбирается он;

  • в противном случае выбирается язык, заданный первым в свойстве cuba.availableLocales .

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

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.logDir

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

Значение по умолчанию для блоков Middleware, Web Client, Web Portal: ${catalina.home}/logs, что в случае стандартного развертывания в Tomcat означает каталог tomcat/logs.

Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/logs.

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.mainMessagePack

Задает главный пакет сообщений данного блока приложения.

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

Используется во всех стандартных блоках.

Пример:

cuba.mainMessagePack=com.haulmont.cuba.web com.sample.sales.web
cuba.manualScreenSettingsSaving

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

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

Интерфейс: ClientConfig

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

cuba.menuConfig

Конфигурационный параметр, задает набор файлов menu.xml .

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

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

Пример:

cuba.menuConfig=cuba-web-menu.xml web-menu.xml
cuba.metadataConfig

Конфигурационный параметр, задает набор файлов metadata.xml .

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

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

Пример:

cuba.metadataConfig=cuba-metadata.xml metadata.xml
cuba.passwordEncryptionModule

Задает имя бина, используемого для хэширования паролей пользователей.

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

Используется во всех стандартных блоках.

cuba.passwordPolicyEnabled

Определяет, нужно ли применять политику проверки пароля. Если свойство имеет значение true, то все новые задаваемые пользователями пароли будут проверяться в соответствии со свойством cuba.passwordPolicyRegExp .

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

Интерфейс: ClientConfig

Используется в блоках клиентского уровня: Web Client, Web Portal, Desktop Client.

cuba.passwordPolicyRegExp

В данном свойстве задается регулярное выражение, которое используется в политике проверки пароля.

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

((?=.*\\d)(?=.*\\p{javaLowerCase}) (?=.*\\p{javaUpperCase}).{6,20})

Это означает, что в пароль должен содержать от 6 до 20 символов, в нем можно использоваться цифры, символы и буквы латинского алфавита. При этом обязательно в пароле должна быть хотя бы одна цифра, одна буква в нижнем регистре и одна буква в верхнем регистре. Более подробная информация о синтаксисе регулярных выражений можно найти на сайтах: http://ru.wikipedia.org/wiki/Регулярные_выражения и http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html

Интерфейс: ClientConfig

Используется в блоках клиентского уровня: Web Client, Web Portal, Desktop Client.

cuba.permissionConfig

Конфигурационный параметр, задает набор файлов permissions.xml .

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

Пример:

cuba.permissionConfig=cuba-web-permissions.xml web-permissions.xml
cuba.persistenceConfig

Конфигурационный параметр, задает набор файлов persistence.xml .

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

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

Пример:

cuba.persistenceConfig=cuba-persistence.xml persistence.xml
cuba.portal.anonymousUserLogin

Логин пользователя системы, который используется для создания анонимной пользовательской сессии в блоке Web Portal.

Пользователь с таким логином должен быть создан в подсистеме безопасности, и ему должны быть назначены соответствующие права. Пароль пользователя игнорируется, так как анонимная сессия портала создается методом loginTrusted() с передачей пароля, указанного в свойстве cuba.trustedClientPassword .

Интерфейс: PortalConfig

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

cuba.randomServerPriority

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

См. также свойство cuba.connectionUrlList.

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

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

cuba.remotingSpringContextConfig

Конфигурационный параметр, задает набор файлов remoting-spring.xml в блоке Middleware.

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

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

Пример:

cuba.remotingSpringContextConfig=cuba-remoting-spring.xml remoting-spring.xml
cuba.rest.productionMode

Включает режим экспуатации REST API, при котором текст исключительных ситуаций не возвращается клиенту.

Интерфейс: RestConfig

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

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

cuba.rest.apiVersion

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

Интерфейс: RestConfig

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

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

cuba.restApiUrl

URL, по которому доступен REST API приложения.

Интерфейс: GlobalConfig

Может использоваться во всех стандартных блоках.

Значение по умолчанию: http://localhost:8080/app-portal/api

cuba.restServicesConfig

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

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

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

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

Значение по умолчанию: cuba-rest-services.xml

Пример:

cuba.restServicesConfig = cuba-rest-services.xml app-rest-services.xml
cuba.schedulingActive

Включает и выключает механизм выполнения назначенных заданий CUBA.

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

Интерфейс: ServerConfig

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

cuba.springContextConfig

Конфигурационный параметр, задает набор файлов spring.xml в каждом стандартном блоке приложения.

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

Используется во всех стандартных блоках.

Пример:

cuba.springContextConfig=cuba-spring.xml spring.xml
cuba.supportEmail

Задает email, на который отправляются отчеты об исключениях из окна стандартного обработчика, и сообщения пользователей из экрана Help -> Feedback.

Если данное свойство установлено в пустую строку, кнопка Report в окне обработчика исключений не показывается.

Для успешной отсылки email необходимо настроить параметры, описанные в разделе Раздел 4.7.2.3, «Настройка параметров отправки email»

Интерфейс: WebConfig

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

cuba.systemInfoScriptsEnabled

Разрешает показ SQL-скриптов добавления/изменения/извлечения экземпляра сущности в окне System Information.

Данные скрипты фактически показывают содержимое строк базы данных, хранящих выбранный экземпляр сущности, независимо от настроек безопасности, в которых некоторые атрибуты могут быть запрещены. Поэтому рекомендуется либо отобрать право на CUBA / Generic UI / System Information для всех ролей пользователей, кроме администраторов, либо установить свойство cuba.systemInfoScriptsEnabled для всего приложения в false.

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

Интерфейс: ClientConfig

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

cuba.tempDir

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

Значение по умолчанию для блоков Middleware, Web Client, Web Portal: ${catalina.home}/temp/${cuba.webContextName}, что в случае стандартного развертывания в Tomcat означает расположение внутри каталога tomcat/temp в подкаталоге с именем текущего веб-приложения, например, tomcat/temp/app-core.

Значение по умолчанию для блока Desktop Client: ${cuba.desktop.home}/temp.

Интерфейс: GlobalConfig

Используется во всех стандартных блоках.

cuba.themeConfig

Задает набор файлов *-theme.properties, в которых описаны переменные тем, такие как размеры диалоговых окон и ширина полей ввода по умолчанию.

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

Значение по умолчанию для Web Client: havana-theme.properties halo-theme.properties

Значение по умолчанию для Desktop Client: nimbus-theme.properties

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

cuba.triggerFilesCheck

Позволяет отключить обработку триггер-файлов вызова бинов.

Триггер-файл представляет собой файл, помещаемый в подкаталог triggers временного каталога данного блока приложения. Имя триггер-файла состоит из двух частей, разделенных точкой. Первая часть соответствует имени бина, вторая - имени вызываемого метода бина, например cuba_Messages.clearCache. Обработчик триггер-файлов следит за их появлением, вызывает соответствующие методы и удаляет файлы.

В платформе вызов обработчика задан в файле cuba-web-spring.xml, то есть по умолчанию обработка триггер-файлов производится для блока Web Client. На уровне проекта можно аналогично запустить обработку для других модулей, периодически вызывая метод process() бина cuba_TriggerFilesProcessor.

См. также свойство cuba.triggerFilesCheckInterval.

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

Используется в блоках, для которых настроена обработка, по умолчанию - Web Client.

cuba.triggerFilesCheckInterval

Устанавливает период в миллисекундах обработки триггер-файлов вызова бинов, заданный в файле cuba-web-spring.xml.

См. также свойство cuba.triggerFilesCheck.

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

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

cuba.trustedClientPassword

Пароль, используемый методом LoginService.loginTrusted(). Средний слой может аутентифицировать пользователей, подключающихся через доверенный клиентский блок, без проверки пользовательского пароля.

Это свойство используется в случае, если пароли пользователей не хранятся в БД, и реальную аутентификацию выполняет сам клиентский блок, например, путем интеграции с Active Directory.

Интерфейсы: ServerConfig, WebConfig

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

cuba.trustedClientPermittedIpMask

Маска IP адресов, с которых возможен вызов метода LoginService.loginTrusted(), в формате регулярного выражения.

Значение по умолчанию: 127\.0\.0\.1

Интерфейсы: ServerConfig, WebConfig

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

cuba.uniqueConstraintViolationPattern

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

ERROR: duplicate key value violates unique constraint "(.+)"

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

IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists

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

Значение по умолчанию: возвращается методом PersistenceManagerService.getUniqueConstraintViolationPattern() для соответствующей СУБД.

Используется во всех клиентских блоках приложения.

cuba.useCurrentTxForConfigEntityLoad

Если значение данного свойства true, то при загрузке экземпляров сущностей через конфигурационные интерфейсы будет использоваться текущая транзакция (если таковая имеется в данный момент), что может положительно сказаться на производительности. В противном случае всегда создается и завершается новая транзакция и возвращается detached экземпляр.

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

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

cuba.useLocalServiceInvocation

При установке данного свойства в true и в случае быстрого развертывания в Tomcat блоки Web Client и Web Portal вызывают сервисы Middleware в обход сетевого стека, что положительно сказывается на производительности системы. В других вариантах развертывания данное свойство необходимо установить в false.

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

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

cuba.user.fullNamePattern

Задает шаблон формирования полного имени пользователя.

Значение по умолчанию: {FF| }{LL}

Полное имя можно сформировать по шаблону из имени, отчества и фамилии пользователя. В шаблоне используются следующие правила:

  • Фигурными скобками {} разделяются части шаблона между собой

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

    LL означает фамилию пользователя, написанную в полном варианте (Иванов)

    L означает фамилию пользователя, написанную в кратком варианте (И)

    FF означает имя пользователя, написанного в полном варианте (Петр)

    F означает фамилию пользователя, написанную в кратком варианте (П)

    MM означает отчество пользователя, написанное в полном варианте (Сергеевич)

    M означает отчество пользователя, написанное в кратком варианте (С)

  • После символа | могут идти любые символы, в том числе, и пробел.

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

cuba.user.namePattern

Задает шаблон отображения имени экземпляра сущности User (пользователь). Данное имя отображается в том числе в правом верхнем углу главного окна системы.

Значение по умолчанию: {1} [{0}]

Вместо {0} подставляется атрибут login, вместо {1} - атрибут name.

Используется в блоках Middleware, Web Client, Desktop Client.

cuba.userSessionExpirationTimeoutSec

Задает таймаут неактивности сессии пользователя в секундах.

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

Интерфейс: ServerConfig

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

Рекомендуется выставлять параметры cuba.userSessionExpirationTimeoutSec и cuba.httpSessionExpirationTimeoutSec в одинаковое значение.

cuba.userSessionProviderUrl

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

Этот параметр необходимо устанавливать в дополнительных блоках среднего слоя, которые выполняют запросы клиентов, но не содержат общего кэша пользовательских сессий. Тогда в начале выполнения запроса при отсутствии требуемой сессии в локальном кэше данный блок вызовет метод LoginService.getSession() по указанному URL, и в случае успеха закэширует полученную сессию у себя.

Интерфейс: ServerConfig

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

cuba.viewsConfig

Конфигурационный параметр, задающий набор файлов views.xml, автоматически развертываемых на старте приложения. См. Раздел 4.2.3, «Представления»

Используется во всех стандартных блоках.

Пример:

cuba.viewsConfig=cuba-views.xml reports-views.xml views.xml
cuba.webAppUrl

URL, по которому доступен Web Client приложения.

Используется, в частности, для формирования ссылок на экраны приложения извне, а также классом ScreenHistorySupport.

Интерфейс: GlobalConfig

Может использоваться во всех стандартных блоках.

Значение по умолчанию: http://localhost:8080/app

cuba.windowConfig

Конфигурационный параметр, задает набор файлов screens.xml .

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

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

Пример:

cuba.windowConfig=cuba-web-screens.xml web-screens.xml
cuba.web.allowHandleBrowserHistoryBack

Позволяет обрабатывать в приложении нажатия на кнопку Back браузера путем переопределения метода AppWindow.onHistoryBackPerformed(). Если свойство установлено в true, стандартное поведение браузера заменяется на вызов этого метода.

См. Раздел 4.5.8, «Специфика Web Client».

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

Интерфейс: WebConfig

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

cuba.web.appFoldersRefreshPeriodSec

Период по умолчанию обновления папок приложения в секундах.

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

Интерфейс: WebConfig

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

cuba.web.appWindowMode

Задает начальный режим главного окна: с вкладками или одноэкранный (TABBED или SINGLE). В одноэкранном режиме экран, открываемый в режиме NEW_TAB, отображается не в новой вкладке, а полностью заменяет текущий экран. Это может быть удобно для простых приложений и неопытных пользователей.

Пользователь впоследствии может задать желаемый режим через экран Help > Settings.

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

Интерфейс: WebConfig

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

cuba.web.foldersPaneDefaultWidth

Ширина по умолчанию панели папок в пикселях.

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

Интерфейс: WebConfig

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

cuba.web.foldersPaneEnabled

Если false, то функциональность панели папок отключена.

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

Интерфейс: WebConfig

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

cuba.web.foldersPaneVisibleByDefault

Если true, то при первом входе пользователя в систему панель папок будет отображаться в развернутом состоянии, если false - то в свернутом.

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

Интерфейс: WebConfig

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

cuba.web.linkHandlerActions

Определяет список команд, передаваемых в URL, для которых вызывается обработка бином LinkHandler. См. Раздел 4.7.13, «Ссылки на экраны»

Элементы списка отделяются символом |.

Значение по умолчанию: open|o

Интерфейс: WebConfig

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

cuba.web.loginDialogDefaultUser

Задает имя пользователя по умолчанию. Оно будет автоматически подставляться в экране входа в систему, что удобно в процессе разработки приложения. В режиме эксплуатации приложения в данном свойстве необходимо задать значение <disabled>.

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

Интерфейс: WebConfig

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

cuba.web.loginDialogDefaultPassword

Задает пароль пользователя по умолчанию. Он будет автоматически подставляться в экране входа в систему, что удобно в процессе разработки приложения. В режиме эксплуатации приложения в данном свойстве необходимо задать значение <disabled>.

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

Интерфейс: WebConfig

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

cuba.web.maxTabCount

Задает максимальное количество вкладок с экранами, которые пользователь может открыть в главном окне приложения. Значение 0 снимает ограничение.

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

Интерфейс: WebConfig

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

cuba.web.rememberMeEnabled

Управляет отображением флажка Remeber Me в стандартном экране входа в систему в веб клиенте.

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

Интерфейс: WebConfig

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

cuba.web.resourcesRoot

Задает расположение каталога, из которого могут быть загружены файлы для вывода на экран компонентом Embedded. Например:

cuba.web.resourcesRoot=${cuba.confDir}/resources

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

Интерфейс: WebConfig

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

cuba.web.showBreadCrumbs

Позволяет скрыть панель breadcrumbs, которая раполагается в верхней части рабочей области главного окна.

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

Интерфейс: WebConfig

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

cuba.web.showFolderIcons

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

  • icons/app-folder-small.png - для папок приложения

  • icons/search-folder-small.png - для папок поиска

  • icons/set-small.png - для наборов

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

Интерфейс: WebConfig

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

cuba.web.table.cacheRate

Регулирует кэширование данных компонента Table в браузере. Количество закэшированных строк будет равняться cacheRate умноженному на pageLength как снизу так и сверху видимой области.

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

Интерфейс: WebConfig

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

cuba.web.table.pageLength

Устанавливает количество строк, которое загружается с сервера в браузер когда компонент Table отрисовывается первый раз после обновления. См. также cuba.web.table.cacheRate.

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

Интерфейс: WebConfig

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

cuba.web.theme

Задает имя темы, используемой по умолчанию в веб клиенте. См. также свойство cuba.themeConfig.

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

Интерфейс: WebConfig

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

cuba.web.useFontIcons

При включенном свойстве для темы halo в качестве пиктограмм стандартных действий и экранов платформы используются элементы шрифта Font Awesome вместо файлов изображений.

Соответствие между именем, указанным в свойстве icon действия или визуального компонента, и элементом шрифта, задается в файле halo-theme.properties платформы. В нем ключи, начинающиеся с cuba.web.icons соответствуют именам пиктограмм, а их значения - константам перечисления com.vaadin.server.FontAwesome. Например, элемент шрифта для пиктограммы стандартного действия create, задается строкой:

cuba.web.icons.create.png = FILE_O

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

Интерфейс: WebConfig

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

cuba.web.useInverseHeader

Для темы Halo или ее наследников управляет цветом заголовка веб-клиента. Если true, то заголовок темный (инверсный), если false - заголовок приобретает цвет основного фона приложения.

Данное свойство не действует, если в теме установлена переменная

$v-support-inverse-menu: false;

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

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

Интерфейс: WebConfig

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

cuba.web.useLightHeader

Включает формирование компактной вехней части окна - лого, строка меню, имя пользователя и кнопка логаута в одну строку. В выключенном состоянии методом AppWindow.createTitleLayout() формируется дополнительная область сверху.

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

Интерфейс: WebConfig

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

cuba.web.viewFileExtensions

Задает список расширений файлов, отображаемых в окне браузера при выгрузке файла через ExportDisplay.show(). Разделителем элементов списка является символ |.

Значение по умолчанию: htm|html|jpg|png|jpeg|pdf

Интерфейс: WebConfig

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

cuba.web.showBreadCrumbs

Позволяет отключить панель навигации (breadcrumbs).

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

Интерфейс: WebConfig

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

cuba.webContextName

Конфигурационный параметр, задающий имя контекста веб-приложения. Как правило, эквивалентен имени каталога или WAR-файла, содержащего данный блок приложения.

Интерфейс: GlobalConfig

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

Например, для блока Middleware, расположенного в каталоге tomcat/webapps/app-core, и доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:

cuba.webContextName=app-core
cuba.webHostName

Конфигурационный параметр, задающий имя хоста, на котором запущен данный блок приложения.

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

Интерфейс: GlobalConfig

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

Например, для блока Middleware, доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:

cuba.webHostName=somehost
cuba.webPort

Конфигурационный параметр, задающий имя порта, на котором запущен данный блок приложения.

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

Интерфейс: GlobalConfig

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

Например, для блока Middleware, доступного по URL http://somehost:8080/app-core данное свойство должно быть задано следующим образом:

cuba.webPort=8080

Приложение C. Системные свойства

Системные свойства задаются при запуске JVM с помощью аргумента командной строки -D и могут быть получены или установлены методами getProperty(), setProperty() класса System.

log4j.configuration

Определяет местонахождение файла конфигурации фреймворка Apache log4j .

Для блоков приложения, работающих на веб-сервере Tomcat, данное системное свойство задается в файлах tomcat/bin/setenv.bat и tomcat/bin/setenv.sh. По умолчанию оно указывает на конфигурационный файл tomcat/conf/log4j.xml.

Для Desktop Client, если данное свойство не задано при запуске JVM, оно задается в коде самого приложения и по умолчанию указывает на файл cuba-log4j.xml, расположенный в корне CLASSPATH. Задать другой файл конфигурации можно, переопределив метод getDefaultLog4jConfig() класса com.haulmont.cuba.desktop.App.

cuba.desktop.home

Для блока Desktop Client задает расположение домашнего каталога, в котором по умолчанию находятся каталоги, определяемые свойствами приложения cuba.confDir , cuba.logDir , cuba.tempDir , cuba.dataDir .

Если данное свойство не задано при запуске JVM, то будет использовано значение ${user.home}/.haulmont/cuba, которое можно изменить в прикладном проекте, переопределив метод getDefaultHomeDir() класса com.haulmont.cuba.desktop.App.

cuba.unitTestMode

Данное системное свойство устанавливается в значение true в режиме выполнения интеграционных тестов базовым классом CubaTestCase.

Пример использования:

if (!Boolean.valueOf(System.getProperty("cuba.unitTestMode")))
  return "Not in test mode";

Основные определения и понятия

А

Артефакт

В контексте данного руководства под артефактом понимается файл (обычно JAR или ZIP), содержащий исполняемый или другой код, получившийся в результате сборки проекта. Артефакт имеет соответствующее некоторым правилам имя и версию, и может хранится в репозитории артефактов.

Б

БД

Реляционная база данных

Браузер сущностей

Экранная форма, на которой размещается таблица со списком сущностей, а также кнопки создания, редактирования, удаления сущности.

В

Внедрение зависимости

Известно также как принцип Inversion Of Control (IoC). Механизм для получения ссылок на используемые объекты, при котором объект только декларирует, от каких объектов он зависит, а контейнер создает нужные объекты и инжектирует в зависимый объект.

См. http://ru.wikipedia.org/wiki/Внедрение_зависимости

Г

Главный пакет сообщений

См. Раздел 4.2.9.2, «Главный пакет сообщений»

З

Загрузка по требованию

См. Раздел 4.4.4.3, «Загрузка по требованию»

И

Источник данных

См. Раздел 4.5.3, «Источники данных»

К

Контейнер

Контейнер управляет жизненным циклом и конфигурацией программных объектов. Является базовым компонентом технологии Dependency Injection (или Inversion of Control).

В платформе CUBA используется контейнер Spring Framework. Для получения более подробной информации см. «Дополнительные материалы»

Контроллер экрана

Java класс, содержащий логику инициализации и обработки событий экрана. Связан с XML-дескриптором экрана.

См. Раздел 4.5.1.3, «Контроллер экрана»

Л

Локальный атрибут

Атрибут сущности, не являющийся ссылкой или коллекцией ссылок на другую сущность. Значения всех локальных атрибутов сущности, как правило, хранятся в одной таблице (исключение составляют некоторые стратегии наследования сущностей).

П

Пакет локализованных сообщений

См. Раздел 4.2.9.1, «Пакеты сообщений»

Персистентный контекст

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

См. Раздел 4.4.4.1, «EntityManager»

Представление

См. Раздел 4.2.3, «Представления»

Р

Рабочий каталог

Локальный каталог файловой системы, в котором содержится проект приложения. Содержит скрипты сборки build.gradle, settings.gradle и проектные файлы IDE.

См. Глава 5, Разработка приложений

Репозиторий артефактов

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

С

Сущность

Основной элемент модели данных, см. Раздел 4.2.1, «Модель данных»

Э

Жадная загрузка

Загрузка данных подклассов и связанных объектов одновременно с основной запрашиваемой сущностью.

A

Application Tiers

См. Раздел 4.1.1, «Уровни и блоки приложения»

Application Properties

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

Application Units

См. Раздел 4.1.1, «Уровни и блоки приложения»

E

Eager Fetching

См. Жадная загрузка .

EntityManager

Программный компонент среднего слоя, служащий для работы с персистентными сущностями.

См. Раздел 4.4.4.1, «EntityManager»

I

Interceptor

Элемент AOP (Aspect Oriented Programming), позволяющий изменить или расширить обычный вызов метода объекта.

См. http://en.wikipedia.org/wiki/Interceptor_pattern

J

Java EE Web Profile

Упрощенный профиль Java Enterprise Edition, разработанный для веб-приложений, для которых не требуются такие технологии как EJB, JTA и т.д.

JMX

Java Management Extensions − технология, которая предоставляет инструменты для управления приложениями, объектами системы, устройствами. Определяет стандарт для написания JMX-компонентов − MBeans.

Более подробную информацию можно найти по адресу: http://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html

См. также Раздел 6.4, «Использование инструментов JMX»

JPA

Java Persistence API - стандартная спецификация технологии объектно-реляционного отображения (ORM). В платформе CUBA используется фреймворк Apache OpenJPA, реализующий эту спецификацию.

JPQL

Платформо-независимый объектно-ориентированный язык запросов, определенный как часть спецификации JPA.

Более подробную информацию можно найти по адресу: http://openjpa.apache.org/builds/2.2.0/apache-openjpa/docs/jpa_langref.html

M

Managed Beans

Программные компоненты Middleware , содержащие бизнес-логику приложения.

См. Раздел 4.2.4, «Управляемые бины»

MBeans

Managed Beans, имеющие JMX-интерфейс. Как правило, имеют внутреннее состояние (например, кэш, конфигурационные данные или статистику), к которому нужно обеспечить доступ через JMX.

Middleware

Средний слой − уровень приложения, содержащий бизнес-логику, работающий с базой данных, и предоставляющий общий интерфейс для верхних (клиентских) уровней приложения.

O

Optimistic locking

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

См. также http://en.wikipedia.org/wiki/Optimistic_concurrency_control

ORM

Object-Relational Mapping - объектно-реляционное отображение - технология связывания таблиц реляционной базы данных с объектами языка программирования.

См. Раздел 4.4.4, «Слой ORM»

P

POJO

Plain Old Java Object − «простой Java-объект в старом стиле» − Java-объект, не унаследованный ни от какого специфического класса и не реализующий никаких служебных интерфейсов сверх тех, которые нужны для описания бизнес-логики.

S

Services

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

См. Раздел 4.4.1, «Сервисы»

Single Sign-On, SSO

Технология, при использовании которой пользователь переходит от одного приложения к другому без повторной аутентификации. Интеграция CUBA-приложения с Active Directory позволяет пользователям Windows входить в приложение без ввода имени и пароля.

Soft deletion

См. Раздел 4.2.1.4, «Мягкое удаление»

U

UI

User Interface - пользовательский интерфейс

X

XML-дескриптор

Файл в формате XML, содержащий описание источников данных и расположения визуальных компонентов экрана.

См. Раздел 4.5.1.2, «XML-дескриптор»