Введение

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

Мы рассмотрим самые основы платформы, и разработаем простое, но полнофункциональное приложение для планирования докладов на конференции. Будут рассмотрены все этапы создания веб-приложений: проектирование модели данных и манипуляции с данными, создание сервисов для выполнения бизнес-логики и, наконец, разработка пользовательского интерфейса. В целом, полученных знаний будет достаточно для того, чтобы начать создавать свои собственные приложения с использованием фреймворка CUBA. В процессе мы будем использовать среду разработки CUBA Studio. Пожалуйста, установите её на свой компьютер. Для работы с визуальными дизайнерами активируйте лицензию или ознакомительный режим.
Sample code repo: https://github.com/cuba-platform/sample-session-planner.

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

Используя меню CUBA Studio, создадим пустой CUBA-проект на JDK-8 и назовем его "SessionPlanner”. В этом руководстве мы будем использовать Java 8. При необходимости можно использовать Java 11.

Создание модели данных

Первый этап - создание сущностей модели данных. В нашей модели будет только два класса: Докладчик (Speaker) и Доклад (Session), связанные отношением один-ко-многим, т.е. Каждый докладчик может читать несколько докладов.

Для начала, создадим сущность “Докладчик”. Дизайнер сущности можно вызвать через ссылку на начальной странице проекта в IDE.

Второй способ - щелкнуть правой кнопкой мыши на узле “Data Model” в дереве проекта и выбрать в меню “New -> Entity”.

Введите название сущности: Speaker и создайте атрибуты в соответствии с таблицей ниже:

Название атрибутаТипОбязательный?Другие ограничения целостности
firstNameString (255)Да
lastNameString (255)
emailString (1024)ДаСоответствие формату адреса электронной почты

В CUBA используются стандартные сущности JPA, их можно изменять, используя либо редактор кода, либо визуальный дизайнер. Мы используем второе. Щелкните по кнопке со значком “+” и добавьте атрибуты в сущность, CUBA Studio сгенерирует необходимые члены класса и расставит аннотации.

В CUBA мы можем задать формат текстового отображения сущностей в пользовательском интерфейсе - "имя экземпляра". Для докладчика мы выберем поля “Имя” и “Фамилия”.

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

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

Название атрибутаТипОбязательный?
topicString (255)Да
startDateDateTimeДа
endDateDateTime
speakerСвязь с сущностью “Speaker”, тип связи - ассоциация, многие-к-одномуДа
descriptionString (2000)
Принципиальных различий по сравнению с первой сущностью нет, кроме ссылочного атрибута “speaker”. В интерфейсе значение этого атрибута следует выбирать из списка опций(ранее созданных докладчиков), а не вводить вручную. Стоит сразу указать параметру “Lookup Type” значение “Dropdown” и отметить “Lookup” в поле “Lookup Actions”. В соответствие с этими настройками, ссылка на докладчика будет представлена компонентом LookupPickerFiled - выпадающим списком с возможностью вызвать отдельный экран с перечнем всех докладчиков .

В итоге, определение атрибута “speaker” в визуальном дизайнере должно выглядеть так:

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

Обратимся к endDate. В CUBA расчет этого атрибута можно привязать к жизненному циклу сущности, описав необходимую логику в методе ее класса. Помеченный специальной аннотацией, метод будет вызываться автоматически. Итак, добавим логику для фазы "PrePersist", т.е. перед сохранением данных. Чтобы это сделать, нужно щелкнуть по кнопке “Lifecycle Callbacks” в верхней части дизайнера сущности и выбрать соответствующую фазу жизненного цикла.

Назовем метод “updateEndDate” и пометим его аннотацией @PreUpdate в дополнение к @PrePersist. Для самого расчета времени окончания доклада создадим отдельный публичный статический метод, мы его используем ещё раз в другой части приложения.

public static Date calculateEndDate(Date startDate) {
  return Date.from(startDate.toInstant().plus(1, ChronoUnit.HOURS));
}

Добавим вызов этого метода в обработчик жизненного цикла сущности:

@PrePersist
@PreUpdate
public void updateEndDate() {
endDate = calculateEndDate(startDate);
}

На этом все. Объектная модель данных создана.

Создание базы данных

CUBA Studio генерирует SQL-скрипты необходимые для создания и обновления БД с учетом выбранной СУБД. По умолчанию, на начальных стадиях разработки в CUBA используется СУБД HSQL. Выберите “CUBA -> Generate Database Scripts” в главном меню и SQL-скрипты для создания базы данных будут сгенерированы. Далее вы можете посмотреть текст скриптов перед тем, как сохранить их в файловую систему как часть исходного кода проекта. Все скрипты для создания и изменения БД можно найти в разделе “Main Data Store” дерева проекта CUBA.

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

Нажмите на кнопку “Save and close” для сохранения скриптов. Чтобы запустить их и создать базу данных, нужно выбрать “CUBA -> Create Database” в главном меню IDE. Помимо таблиц приложения, CUBA также создает системные таблицы для хранения информации о пользователях, ролях, привилегиях и т.д.

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

Создание стандартных экранов

В CUBA Studio есть генератор пользовательского интерфейса. С его помощью мы создадим простые, но полезные экраны:

  • Браузер - для просмотра данных в табличной форме
  • Редактор - для редактирования одной строки данных

Сначала создадим экраны для работы с докладчиками. Сущность “Докладчик” довольно простая, поэтому для неё оставим настройки экранов по умолчанию. Запустите генератор, щелкнув на пункте “Create Screen” меню “Screens” в верхней части дизайнера сущностей.

Генератор экранов также можно запустить, щелкнув правой кнопкой мыши на сущности в дереве проекта и выбрав “New -> Screen” из контекстного меню.

Для сущности “Докладчик” мы создадим браузер и редактор. Выберите пункт “Browser and Editor” на вкладке “Screen Templates” мастера создания экранов и нажмите кнопку “Next”.

На этом этапе оставьте все значения параметров по умолчанию и нажмите “Next”.

Здесь вы можете поменять заголовки экранов и название пункта меню приложения, если необходимо. После этого нажмите кнопку “Finish” для завершения создания стандартных экранов.

Можно видеть, что в коде проекта каждый экран состоит из двух частей: контроллера, написанного на языке Java, который отвечает за логику работы экрана, и разметки в формате XML, которая отвечает за внешний вид и расположение элементов экрана. В нашем случае, браузер состоит из файлов “speaker-browse.xml” и “SpeakerBrowse.java”, а редактор - “speaker-edit.xml” и “SpeakerEdit.java” соответственно. Файлы можно найти в разделе “Generic UI -> Screens” в дереве проекта CUBA.

Отметьте для себя раздел “DATA” в XML дескрипторе - он определяет, как данные выбираются из базы данных.

<data readOnly="true">
   <collection id="speakersDc"
               class="com.company.sessionplanner.entity.Speaker"
               view="_local">
       <loader id="speakersDl">
           <query>
               <![CDATA[select e from sessionplanner_Speaker e]]>
           </query>
       </loader>
   </collection>
</data>

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

Создание браузера и редактора докладов

Теперь запустим мастер создания экранов для сущности “Доклад”, так же как и ранее, выберем раздел “Entity browser and editor screens” и нажмем кнопку “Next”. На следующем шаге нужно создать представления для просмотра и редактирования сущностей. В CUBA представление определяет, какие атрибуты сущности (а также связанные сущности) необходимо извлекать из базы данных. Эта информация используется не только при создании экранов, но также и для правильного дизайна API приложения.

Создадим отдельное представление для сущности “Доклад”, включив в него докладчика. В мастере создания экранов в выпадающем списке “Browse View” выберите “Create view…”

В появившемся диалоговом окне введите название представления: “session-browse-view”, а в списке атрибутов ниже выберите “speaker”. В итоге окно создания представления должно выглядеть так:

Выбирая атрибут “speaker”, мы даем команду загружать данные ещё и из таблицы “Speaker”, а конкретно - атрибуты “firstName” и “lastName”, чтобы показывать строковое представление докладчика на экране с докладами.

Нажмите кнопку “OK” для сохранения представления.

Для доклада мы не будем показывать дату и время окончания в режиме редактирования, поскольку будем генерировать это значение автоматически. Создадим представление для редактирования аналогично тому, как мы делали это ранее, и назовем его “session-edit-view”. В поле “Extends” необходимо выбрать значение “_minimal”, а затем в списке выделить все атрибуты, кроме “endDate”. CUBA не будет загружать данные о дате и времени окончания из базы, но будет сохранять вычисленное значение во время создания и обновления доклада, как было указано в разделе “Создание вычисляемого атрибута”. Итоговый вид редактора представления представлен на иллюстрации.

Нажмите “OK”, чтобы сохранить представление.

Теперь можем завершить создание экранов. Нажмите “Next”, чтобы перейти к этапу задания заголовков экранов и названия пункта меню, а затем нажмите “Finish”. Если вы посмотрите на дизайн экрана в режиме предварительного просмотра, то увидите, что поле “Description” нуждается в небольшом изменении, поскольку оно было сгенерировано как обычное однострочное поле ввода.

Давайте изменим вид поля ввода, сделав его многострочным. Самый простой способ это сделать - заменить компонент в дескрипторе экрана. Давайте поменяем тэг “textField” на “textArea” для поля с id=“descriptionField”.

<form id="form" dataContainer="sessionDc">
   <column width="250px">
       <textField id="topicField" property="topic"/>
       <dateField id="startDateField" property="startDate"/>
       <lookupPickerField id="speakerField" optionsContainer="speakersDc" property="speaker">
           <actions>
               <action id="lookup" type="picker_lookup"/>
           </actions>
       </lookupPickerField>
       <textArea id="descriptionField" property="description"/>
   </column>
</form>

Обратите внимание, что в редакторе доклада нет элемента для атрибута “endDate”, а в браузере он есть.

Первый запуск

Чтобы запустить приложение, можно воспользоваться кнопкой на панели инструментов IDE

Также можно запустить его из главного меню: “CUBA -> Start Application Server”.

После старта сервера приложение можно открыть в веб-браузере, используя URL, который показан в окне запуска. В нашем случае, это будет http://localhost:8080/app. После открытия будет выведено окно логина, имя пользователя и пароль по умолчанию - “admin” - “admin”.
Экраны приложения можно открыть из меню “Application”.
Давайте добавим тестовые данные: двух докладчиков и два доклада, которые запланируем на следующий день. В качестве эксперимента можно ввести неправильный email для докладчика, чтобы убедиться, что валидация работает как нужно.

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

Разработка пользовательского интерфейса

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

Откроем дескриптор “session-browse.xml” в дизайнере и в палитре компонентов найдем “Tab Sheet”.

Теперь перетащите компонент “TabSheet” под компонент “filter” в разметке и назначьте ему id - “sessionsTabs”, используя закладку “Properties”.

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

Теперь перетащите два компонента “Tab” под “sessionsTabs”, назначьте им id: “calendarTab” и “tableTab” и заголовки “Sessions Calendar” и “Sessions Table” соответственно.

Затем сверните “sessionsTable” и перетащите этот компонент на закладку “tableTab”.

Можно увидеть, что интерфейс экрана “сломался” и фильтр занимает примерно половину пространства экрана.

Чтобы это исправить, нужно определить компонент, который будет занимать всю свободную площадь экрана. В дереве выберите компонент “layout” и установите его свойству “expand” значение “sessionTabs”, как показано на рисунке. Это позволит листу с закладками занять всю свободную площадь экрана.

Теперь найдем компонент “Calendar” в палитре и перенесем его на закладку “calendarTab”. Назначим id - “sessionsCalendar” для вновь добавленного компонента.

Установите значение свойства “expand” компонента “tableTab” в “sessionsTable”, а для “calendarTab” - “sessionsCalendar”.

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

Давайте свяжем календарь с коллекцией данных, которая загружается из БД в экран. Свойству “dataContainer” календаря установите значение “sessionsDc”. Затем свяжите

  • Свойство startDateProperty с датой и временем начала доклада
  • Свойство endDateProperty с датой и временем окончания доклада
  • Свойство captionProperty с темой доклада
  • И свойство descriptionProperty с описанием доклада

Чтобы немного попрактиковаться с редактором разметки XML, поменяем свойства календаря. Пусть он показывает только рабочие часы (с 8 до 18), а также сделаем кнопки навигации по неделям видимыми. Обратите внимание, что редактор разметки XML поддерживает автодополнение имен свойств и тэгов, что значительно упрощает работу с разметкой и, зачастую, помогает делать изменения быстрее, чем в визуальном редакторе.
Текст исправлений в XML выделен жирным шрифтом:

<calendar id="sessionsCalendar" dataContainer="sessionsDc" startDateProperty="startDate"
         endDateProperty="endDate" captionProperty="topic" descriptionProperty="description"
   firstVisibleHourOfDay="8" lastVisibleHourOfDay="18" navigationButtonsVisible="true"/>

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

Использование экранного API

Когда мы взаимодействуем с пользовательским интерфейсом, например, щелкая мышью по элементам, изменяя размеры экрана, вводя текст и т.д., генерируются специальные события. Фреймворк CUBA предоставляет API, который позволяет, подписавшись на эти события, определить реакцию на них. Давайте обработаем событие “щелчок левой кнопкой мыши” по докладу в календаре - мы будем вызывать редактор доклада. Для этого мы будем пользоваться API для работы с экранами, который также предоставляет фреймворк.

В контроллере браузера докладов нажмите на кнопку “Subscribe to Event” в верхней части окна редактора кода.

Выберите событие “CalendarEventClickEvent” в диалоговом окне, нажмите OK.

Будет сгенерирован пустой метод.

@Subscribe("sessionsCalendar")
private void onSessionsCalendarCalendarEventClick(Calendar.CalendarEventClickEvent event) {

}

Чтобы использовать API, нужно получить доступ к сервису, для этого в контроллере экрана нажмите на кнопку “Inject” в верхней части окна редактора и выберите этот сервис из секции Screen API во всплывающем окне.

Вызвать меню добавления сервиса (и подписки на события) можно, нажав комбинацию клавиш Alt+insert прямо в коде контроллера и выбрать “Inject” (или “Subscribe to Event”) во всплывающем меню.

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

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

При щелчке по докладу в календаре нужно вызвать редактор, для которого в качестве родительского экрана выступает текущий (this)
screenBuilders.editor(Session.class, this)

Затем извлекаем доклад из объекта события.
java.editEntity((Session) event.getEntity())

Редактор необходимо открыть в диалоговом режиме.
java.withLaunchMode(OpenMode.DIALOG)

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

@Subscribe("sessionsCalendar")
private void onSessionsCalendarCalendarEventClick(Calendar.CalendarEventClickEvent event) {
   Screen screen = screenBuilders.editor(Session.class, this)
           .editEntity((Session) event.getEntity())
           .withLaunchMode(OpenMode.DIALOG).build();
   screen.addAfterCloseListener(afterCloseEvent -> {
       getScreenData().loadAll();
   });
   screen.show();
}

Теперь переоткрываем браузер докладов и вызываем редактор, щелкнув левой кнопкой мыши по докладу в календаре.

Как видим, необходимо поправить ширину и высоту редактора. В IDE откройте дескриптор экрана, выберите “dialogMode” в дереве компонентов и установите свойствам “width” и “height” значение “auto”.

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

Добавление бизнес-логики

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

Правой кнопкой мыши щелкнем на узле “Сервисы” в дереве CUBA проекта и выберем “New ->Service”. Появится мастер создания сервиса. Введем SessionService в качестве имени интерфейса. Название SessionServiceBean для реализации будет сгенерировано автоматически.

Создадим метод “rescheduleSession” в интерфейсе как показано в коде ниже:

public interface SessionService {
   String NAME = "sessionplanner_SessionService";

   boolean rescheduleSession(Session session, Date newStartDate);
}

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

Далее, откройте класс SessionServiceBean в редакторе. Класс можно найти в дереве проекта в разделе “Middleware - Services”:

Класс не имеет реализации методов интерфейса, IDE подсвечивает его как ошибочный.

Внутри тела класса нажмите Alt+Insert и выберите “Implement methods” во всплывающем меню:

Выберите метод “rescheduleSession”, будет сгенерирована заготовка метода.

@Service(SessionService.NAME)
public class SessionServiceBean implements SessionService {

   @Override
   public boolean rescheduleSession(Session session, Date newStartDate) {
       return false;
   }
}

В сервисе мы будем использовать CUBA API для доступа к данным - класс DataManager. С его помощью выполним JPQL запрос для проверки: есть ли для заданного докладчика ещё доклады в указанном промежутке времени. Затем проверим результат выполнения запроса и, в зависимости от него, обновим доклад или вернем false.
Добавьте DataManager в код, нажав Alt+Insert в теле класса и выбрав “Inject” из всплывающего меню.

Выберите DataManager из списка:

Итоговый код метода представлен ниже:

@Override
public boolean rescheduleSession(Session session, Date newStartDate) {

   Date newEndDate = Session.calculateEndDate(newStartDate);

   int sessionsSameTime = dataManager.load(Session.class)
           .query("select s from sessionplanner_Session s where " +
                   "s.startDate < :newEndDate and s.endDate > :newStartDate " +
                   "and s.speaker = :speaker " +
                   "and s.id <> :sessionId")
           .parameter("newStartDate", newStartDate)
           .parameter("newEndDate", newEndDate)
           .parameter("speaker", session.getSpeaker())
           .parameter("sessionId", session.getId())
           .list().size();

   if (sessionsSameTime == 0) {
       session.setStartDate(newStartDate);
       dataManager.commit(session);
       return true;
   }

   return false;
}

Обратите внимание, что мы повторно использовали метод, который был создан ранее в разделе “Создание вычисляемого атрибута”:

Date newEndDate = Session.calculateEndDate(newStartDate);

Сервис готов, давайте добавим его в браузер докладов. Настроим его вызов на событие перетаскивания доклада в календаре. Если доклад не может быть перенесен, мы покажем предупреждение, используя средства CUBA для показа сообщений на экране.

Перейдите в код контроллера браузера докладов и добавьте вновь созданный сервис так же, как мы добавляли API ScreenBuilders. Затем добавьте сервис “Notifications” таким же образом. И в конце добавьте подписку на событие “CalendarEventMove” для календаря, аналогично подписке на событие щелчка кнопкой мыши, когда мы вызывали редактор докладов.

В обработчике события напишем следующий код:

@Subscribe("sessionsCalendar")
private void onSessionsCalendarCalendarEventMove(Calendar.CalendarEventMoveEvent event) {

   Session session = ((EntityCalendarEvent<Session>)event.getCalendarEvent()).getEntity();

   if (!sessionService.rescheduleSession(session, event.getNewStart())) {
       notifications.create(Notifications.NotificationType.WARNING)
       .withCaption("Session "+session.getTopic()+" cannot be rescheduled to "+event.getNewStart()+" due to a conflict")
       .show();
   }

   getScreenData().loadAll();
}

Чтобы сервис заработал, необходимо перезапустить приложение. Это можно сделать при помощи кнопки “Run” панели инструментов IDEA:

После перезапуска открываем календарь докладов и - вуаля! Теперь можем переносить доклады при помощи мыши прямо в календаре. Можно проверить, как это работает, для этого добавим еще один доклад и попробуем его перенести в календаре так, чтобы он пересекся с другим докладом того же докладчика.

Изменение стандартного оформления

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

Откройте файл конфигурации надписей - “messages.properties”, который можно найти в разделе “Generic UI - Main Message Pack” дерева проекта CUBA.

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

application.caption = CUBA Session Planner
application.logoImage = branding/app-icon-menu.png
application.logoLabel = Session Planner

loginWindow.caption = Login
loginWindow.welcomeLabel = Welcome to Session Planner!
loginWindow.logoImage = branding/app-icon-login.png

menu-config.application-sessionplanner = Planner
menu-config.sessionplanner_Speaker.browse=Speakers
menu-config.sessionplanner_Session.browse=Sessions

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

Магазин компонентов

Вместе с фреймворком поставляется магазин компонентов , в котором содержится довольно много дополнений, так что вы можете легко добавить новые возможности в приложение. Например, это может быть поддержка картографии или рисование диаграмм. Эти компоненты можно легко установить из CUBA Studio, используя главное меню: “CUBA -> Marketplace”.

Заключение

Платформа CUBA предоставляет большое количество функциональности, чтобы помочь вам сделать бизнес-приложения быстро и качественно. В этой статье мы рассмотрели только базовые вещи. На нашем сайте cuba-platform.ru можно найти намного больше примеров и учебников, которые помогут вам глубже освоить возможности CUBA, а также форум, на котором можно задавать вопросы касательно разработки на платформе CUBA.

Спасибо за интерес к CUBA!