Содержание
Данный документ является руководством по применению подсистемы Workflow платформы CUBA.
Данное руководство предназначено для разработчиков, создающих на платформе CUBA приложения с элементами управления бизнес-процессами. Предполагается, что читатель ознакомлен с Руководством по разработке приложений, доступным по адресу www.cuba-platform.ru/manual.
Настоящее Руководство, а также другая документация по платформе CUBA доступны по адресу www.cuba-platform.ru/manual.
Если у Вас имеются предложения по улучшению данного руководства, обратитесь, пожалуйста, в службу поддержки по адресу: ru.cuba-platform.com/support/topics.
При обнаружении ошибки в документации укажите, пожалуйста, номер главы и приведите небольшой участок окружающего текста для облегчения поиска.
Сутью Workflow является последовательное изменение состояния некоторой сущности (карточка) в соответствии с заданным процессом, вовлекающее пользователей системы и автоматические механизмы. Типичным примером является процесс согласования некоторого документа между сотрудниками в организации.
Рассмотрим структуру подсистемы Workflow платформы CUBA.
Основными элементами структуры являются:
Process Design (дизайн процесса) - схема процесса, а также набор связанных с ней скриптов, переменных, параметров оповещения и локализации сообщений. Дизайн является исходным материалом для процесса, который создается или обновляется на основе дизайна в результате развертывания последнего в приложении.
Process (процесс) - исполняемое описание бизнес-процесса. Механизм исполнения основан на фреймворке jBPM 4, поэтому основным элементом процесса является описатель графа состояний на языке jPDL. Кроме того, процесс включает в себя описатели форм пользовательского интерфейса, списки пользователей, назначенных для исполнения ролей процесса, локализованные сообщения и т.д.
Card (карточка) - сущность модели данных, непосредственно связанная с экземпляром процесса. В большинстве случаев карточкой процесса может являться объект предметной области, состояние которого меняется в соответствии с процессом. Это, например, документ для согласования, или тикет в системе отслеживания дефектов. В случае если такого объекта в предметной области нет, карточкой может являться искусственная сущность, просто отражающая текущее состояние экземпляра процесса.
Можно сказать, что процесс задает тип бизнес-процесса, а карточка представляет собой один экземпляр этого бизнес-процесса.
Процесс задает набор состояний (узлов) и переходов между ними, а также следующие ассоциированные объекты:
Activities (действия) - программный код, выполняющийся при переходе в некоторое состояние.
Forms (формы) - экраны, позволяющие взаимодействовать с пользователем во время принятия им решений о переходе по процессу.
Timers (таймеры) - программный код, автоматически срабатывающий по истечении заданного времени после входа в некоторое состояние.
Во время выполнения процесса возникают объекты Assignments (назначения), сигнализирующие пользователю о том, что он должен предпринять некоторые действия по процессу.
В подсистему Workflow платформы интегрирован визуальный редактор процессов Visual Designer, который позволяет создавать дизайн процессов и разворачивать их прямо в процессе работы приложения. Набор возможных состояний, действий, форм и таймеров, из которых конструируются процессы, закладывается в приложение на этапе разработки.
Рассмотрим диаграмму классов основных элементов подсистемы Workflow.
Design
- объект, хранящий дизайн процесса.
Proc
- исполняемый процесс. Атрибуты процесса:
design
- ссылка на дизайн, по которому был создан данный процесс.
name
- имя процесса, понятное пользователям. Имя задается на этапе дизайна, однако может быть задано и в экране редактирования процесса.
jbpmProcessKey
- ключ процесса в исполняющем механизме jBPM. Формируется автоматически при развертывании дизайна процесса.
code
- код процесса для обращения к нему программными средствами. По умолчанию равен jbpmProcessKey
, однако может быть изменен в экране редактирования процесса.
messagesPack
- имя пакета сообщений, в котором содержатся локализованные названия состояний, переходов, различных описаний и сообщений процесса. Данный пакет сообщений формируется автоматически на этапе развертывания дизайна процесса и хранится вместе с другими файлами процесса в соответствующем подкаталоге конфигурационного каталога приложения.
roles
- список объектов типа ProcRole
, определяющих роли участников процесса.
ProcRole
- роль в процессе. Объекты данного типа создаются автоматически при развертывании дизайна процесса если он содержит состояния вида Назначение, то есть те, которые выдают задачи пользователям и останавливают выполнение процесса до принятия пользователем необходимых действий. Изначально объекты ProcRole
не связываются ни с какими пользователями системы, однако в экране редактирования процесса для каждой роли можно назначить исполнителя по умолчанию.
DefaultProcActor
- список исполнителей некоторой роли, задаваемый на уровне процесса.
Card
- карточка процесса. Как правило, от класса Card
наследуется некоторая сущность предметной области, которая тем самым приобретает возможность двигаться по процессу. Атрибуты карточки:
description
- опциональное текстовое описание данного экземпляра карточки, позволяющее пользователю различать карточки без загрузки атрибутов конкретного типа, расширяющего Card
. Например, если карточка отражает документ, то в поле description
имеет смысл записать тип, номер и дату этого документа.
proc
- ссылка на процесс.
roles
- список объектов типа CardRole, определяющих исполнителей ролей процесса для данной карточки.
jbpmProcessId
- идентификатор экземпляра процесса в механизме исполнения jBPM.
state
- название текущего состояния процесса.
assignments
- список объектов типа Assignment
, созданных в данном экземпляре процесса.
attachments
- список объектов типа CardAttachment
, позволяющих привязывать к карточке загружаемые во время выполнения процесса файлы.
CardRole
- сущность, определяющая исполнителей ролей процесса для данной карточки. Атрибуты CardRole
:
card
- карточка, для которой определяется исполнитель роли.
procRole
- роль в процессе.
user
- исполнитель роли (типа User
- пользователь системы).
CardAttachment
- вложение, предоставляет возможность ассоциировать с карточкой загружаемые файлы. Атрибуты (вместе с суперклассом Attachment
):
card
- карточка.
assignment
- если файл загружен на этапе выполнения назначения, то этот атрибут содержит ссылку на соответствующий объект Assignment
.
file
- загруженный файл типа FileDescriptor
.
name
- название вложения.
comment
- комментарий к вложению.
Assignment
- назначение. Объекты данного типа создаются в системе при переходе процесса в состояние "назначение". В этом состоянии процесс останавливает выполнение и ожидает команды от пользователя или от автоматического механизма. При получении команды о завершении в назначении заполняются атрибуты finished
, finishedByUser
и outcome
, и процесс переходит в следующее состояние. Для одной карточки и пользователя может существовать не более одного незавершенного назначения. Атрибуты назначения:
card
- карточка.
user
- пользователь системы, для которого выдано назначение.
proc
(процесс) - тот же процесс, который в момент создания назначения установлен для карточки.
name
- наименование назначения.
description
- описание назначения.
jbpmProcessId
- идентификатор экземпляра процесса jBPM, исполняемого по карточке в момент создания назначения.
dueDate
- момент времени, до которого назначение должно быть выполнено.
finished
- фактический момент времени завершения назначения. Равен null
, пока назначение не завершено.
finishedByUser
- пользователь, фактически завершивший назначение. Равен null
, пока назначение не завершено.
outcome
- наименование выхода из состояния назначения, которое выбрал пользователь. Например, в схеме бизнес-процесса может быть узел-назначение, который предписывает пользователю проверить факт исправления некоторого дефекта и выбрать один из двух путей дальнейшего следования процесса: "OK" или "Not OK". Тогда, если пользователь выбрал "OK", то в поле outcome
назначения запишется эта строка, и наоборот.
attachments
- список объектов типа CardAttchment
, которые были созданы для этого назначения.
В данной главе мы рассмотрим применение подсистемы Workflow в приложении-примере Библиотека, который может быть загружен с помощью CUBA Studio.
Задача - добавить в приложение возможность создавать и учитывать электронные версии изданий книг, имеющихся в библиотеке. При этом процесс создания электронной версии должен быть следующим:
Произвольный пользователь системы создает объект EBook
и указывает в нем издание книги, для которого нужно нужно создать электронную версию, а затем стартует процесс оцифровки.
Пользователь с ролью Manager
получает назначение, которое предписывает ему одобрить или запретить оцифровку данной книги. Если менеджер одобрил оцифровку, то процесс продолжается, иначе - завершается.
После одобрения менеджером для данного EBook
создается назначение пользователю с ролью Operator
. Оператор выполняет оцифровку, и в случае успеха прикладывает к EBook
файл с электронной версией книги.
По завершении процесса объект EBook
может находиться в одном из трех состояний:
Done
- оцифровка выполнена успешно, и объект содержит вложенный файл с электронной версией.
Failed
- оцифровка не удалась.
Disapproved
- оцифровка не одобрена менеджером.
Запустите CUBA Studio, перейдите в окно и загрузите проект Library.
Откройте проект Library в Studio.
Откройте окно свойств проекта Base projects включите проект workflow, затем сохраните изменения. Studio предложит пересоздать скрипты Gradle. Согласитесь.
-> и в спискеЗапустите build/tomcat
.
Создайте базу данных приложения:
-> .Запустите сервер приложения:
-> .Откройте веб-интерфейс приложения по адресу http://localhost:8080/app. Войдите в систему с именем admin
и паролем admin
. В главном меню среди прочих должен быть доступен пункт , предоставляющий доступ к экранам управления подсистемой Workflow.
В Studio выполните c:\work\library\library.ipr
в IntelliJ IDEA.
Создадим класс сущности EBook
, представляющий собой электронную версию издания книги. Класс EBook
должен быть унаследован от Card
(карточки процесса), чтобы управлять состоянием EBook
в соответствии с требуемым бизнес-процессом.
Перейдите на вкладку Entities панели навигатора Studio, выберите пакет com.sample.library.entity
и нажмите . Введите следующие значения свойств сущности:
Class name - EBook
Table - LIBRARY_EBOOK
Parent class - Card [wf$Card]
. Для класса Card
определена стратегия наследования InheritanceType.JOINED
, поэтому EBook
будет храниться в отдельной таблице и его первичный ключ будет одновременно внешним ключом, сылающимся на первичный ключ Card
.
Discriminator - 10
. Дискриминатор - это значение поля базового типа, которое будет установлено в базе данных для всех экземпляров конкретного типа. В данном случае для базового класса Card
определены аннотации:
@DiscriminatorColumn(name = "CARD_TYPE", discriminatorType = DiscriminatorType.INTEGER) @DiscriminatorValue("0")
Это означает, что в поле CARD_TYPE должно храниться значение целого типа, и для экземпляров базового типа это будет значение 0
. Поэтому для типа EBook
можно задать любое значение, отличное от 0
.
В поле Primary key join column Studio автоматически сформирует имя CARD_ID для первичного ключа создаваемой сущности.
В поле Referenced primary key column Studio выберет первичный ключ таблицы WF_CARD, хранящей базовую сущность Card
, то есть ID.
Далее, создадим атрибут сущности, содержащий ссылку на издание книги.
Нажмите Create attribute задайте следующие свойства:
под списком атрибутов и в окне
Name - publication
Attribute type - ASSOCIATION
Type - BookPublication [library$BookPublication]
Mandatory - on
. Атрибут будет обязательным для заполнения.
Cardinality - MANY_TO_ONE
. Это означает, что несколько экземпляров EBook
могут быть созданы для одного экземпляра BookPublication
.
В поле Column будет предложено подходящее имя колонки - PUBLICATION_ID.
Сохраните изменения. В результате будет создан следующий класс сущности:
package com.sample.library.entity; import javax.persistence.*; import com.haulmont.workflow.core.entity.Card; @PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID") @DiscriminatorValue("10") @Table(name = "LIBRARY_EBOOK") @Entity(name = "library$EBook") public class EBook extends Card { private static final long serialVersionUID = -7326357893869004530L; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "PUBLICATION_ID") protected BookPublication publication; public void setPublication(BookPublication publication) { this.publication = publication; } public BookPublication getPublication() { return publication; } }
После создания класса сущности Studio сообщит о том, что модель данных изменена по сравнению с текущей схемой базы данных. Требуется сгенерировать скрипты и запустить создание или обновление БД.
Нажмите Entites на панели навигатора. Studio сгенерирует скрипты обновления и инициализации БД, включающие в себя создание таблицы LIBRARY_EBOOK и ее внешних ключей. Так как в нашей БД еще нет никаких данных, и мы можем безболезненно пересоздать ее, скрипты на вкладке Update scripts можно сразу удалить. После этого сохраните изменения.
в секцииОстановите сервер приложения командой
-> . Через несколько секунд станет доступным пункт меню -> , который и нужно выполнить.Создадим стандартные экраны просмотра списка и редактирования сущности EBook
. Для этого сначала определим представления (views) для этих экранов.
Выберите EBook
в секции Entities на панели навигатора и нажмите . Задайте имя представления в поле Name - eBook.browse
. В панели Attributes по умолчанию выбраны все локальные (не ссылочные) атрибуты сущности. Отключите их все и включите единственный интересующий нас на данном этапе атрибут publication
. Так как этот атрибут представляет собой ссылку на сущность BookPublication
, в дереве отобразятся атрибуты этой сущности. Выберите атрибут book
и в правой панели параметров задайте для него представление _minimal
. Сохраните изменения.
Теперь, если открыть файл с представлениями в IDE, в нем можно найти следующий описатель:
<view class="com.sample.library.entity.EBook" name="eBook.browse"> <property name="publication"> <property name="book" view="_minimal"/> </property> </view>
Аналогочно создадим представление eBook.edit
для экрана редактирования. На данном этапе это представление идентично eBook.browse
, однако в будущем они станут различными.
После создания представлений снова выберите EBook
в секции Entities панели навигатора и нажмите . В появившемся окне выберите eBook.browse
для Browse view, eBook.edit
для Edit view, и нажмите . Studio откроет секцию Screens панели навигатора и покажет созданные экраны.
Добавим в главное меню элемент для доступа к списку EBook
.
Откройте секцию Main menu панели навигатора и нажмите для web-menu.xml. Выделите элемент library
и нажмите . В появившемся окне выберите library$EBook.lookup
в поле Id. Задайте заголовок пункта меню, нажав в поле Caption. Сохраните изменения.
После создания стандартных экранов и регистрации в главном меню можно запустить сервер (EBook
.
Перейдем к основной части примера - описанию бизнес-процесса и реализации обработки объектов EBook
в соответствии с ним. В данном разделе мы создадим дизайн процесса и затем развернем его для выполнения.
В веб-интерфейсе запущенного приложения откройте экран Book scanning
, и нажмите . Откроется новое окно браузера CUBA Workflow Designer.
Перетащите на рабочую панель узел Start, а затем узел Assignment. Соедините выход Start со входом Assignment. В узле Assignment задайте имя Approval
и роль Manager
. Нажмите кнопку внутри узла Assignment для создания выхода и дайте ему имя Approve
. Затем добавьте еще один выход - Deny
. В результате во время выполнения процесса при переходе в состояние Approval
пользователю с ролью Manager
будет создано назначение. Процесс остановит выполнение и продолжит его, когда пользователь выберет один из выходов - Approve
или Deny
.
В случае отказа менеджера процесс должен перейти в состояние Disapproved
и завершиться. Для регистрации этого состояния добавьте узел State с именем Disapproved
и соедините его вход с выходом Deny
узла Approval
. Затем добавьте узел End и соедините его с выходом узла Disapproved
. При выполнении процесс запишет состояние Disapproved
в карточку (объект EBook
) и, не останавливаясь, завершит выполнение.
На данном этапе должна получиться следующая схема:
Нажмите
для сохранения промежуточных результатов редактирования.Добавьте еще один узел Assignment и задайте для него имя Scanning
и роль Operator
. Добавьте выходы Success
и Fail
. Соедините вход Scanning
с выходом Approve
узла Approval
. В результате во время выполнения при одобрении менеджером процесс перейдет в состояние Scanning
, остановится и выдаст назначение пользователю с ролью Operator
. Выполнение продолжится, когда пользователь завершит назначение, выбрав один из выходов.
Для регистрации финального состояния процесса добавьте два узла State с именами Done
и Failed
и соедините их с соответствующими выходами узла Scanning
. Затем добавьте еще один узел End и соедините с ним выходы узлов Done
и Failed
.
В итоге схема должна приобрести следующий вид:
При успешном выполнении оцифровки оператор должен приложить к объекту файл с электронной версией. Для реализации этого добавим в процесс форму взаимодействия с пользователем.
Выберите узел Scanning
, в правой панели дизайнера раскройте секцию Forms и нажмите . Установите следующие атрибуты:
Transition - имя выхода, при котором будет показана данная форма. Выберите значение Success
.
Form - тип формы. Выберите значение Transition
.
Hide Attachments - скрыть элементы выбора вложений. Оставьте в выключенном состоянии.
Вид правой панели дизайнера с параметрами формы:
Таким образом, при завершении назначения в направлении Success
перед оператором возникнет диалоговая форма, в которой он сможет добавить вложения - файлы с с электронной версией книги.
Сохраните дизайн процесса и закройте окно браузера CUBA Workflow Designer.
Созданный дизайн процесса необходимо скомпилировать, то есть создать на основе схемы исполняемый код процесса. Выберите строку с дизайном в окне Processes Design и нажмите . В случае успешной компиляции в колонке Compilation date появится текущее время.
Следующий этап - развертывание процесса. Выберите строку со скомпилированным дизайном и нажмите Create new process и нажмите . В результате происходит следующее:
. В диалоговом окне оставьте отмеченным флажокВ базе данных приложения создается новый объект Proc
и соответствующие имеющимся в дизайне ролям объекты ProcRole
.
В подкаталоге process
конфигурационного каталога среднего слоя приложения создается каталог с именем вида proc_<date_time>
, где date_time
- момент времени развертывания процесса. Данный каталог содержит файлы, необходимые для исполнения процесса: jPDL, описатель форм, пакет локализованных сообщений.
Файл jPDL отправляется в механизм исполнения jBPM, который создает соответствующие записи в таблицах JBPM4_DEPLOYMENT и JBPM4_DEPLOYPROP. Идентификатором процесса jBPM становится строка, идентичная имени каталога развертывания (proc_<date_time>
). Данный идентификатор записывается также в атрибут jbpmProcessKey
объекта Proc
.
Процесс готов к запуску, однако для целей нашего примера выполним еще одно подготовительное действие - назначим исполнителей по умолчанию для ролей в процессе.
Откройте экран Manager
и Operator
и в списке Default participants добавьте для них исполнителей - предварительно созданных пользователей системы manager
и operator
соответственно. Экран редактирования процесса примет следующий вид:
Кроме явно указанных в дизайне ролей система создала роль CARD_CREATOR
с признаком Assign to creator. Эту роль можно использовать для того, чтобы пользователь, создавший карточку, автоматически становился участником процесса. В описываемом примере данная роль не используется.
Атрибут Code процесса желательно установить в некоторое осмысленное уникальное значение, так как в программном коде по этому атрибуту удобно находить процесс для запуска. В данном случае задаем значение book_scanning
.
Как правило, дизайн процесса разрабатывается итеративно, поэтому система предоставляет возможность неоднократно развертывать один и тот же изменяющийся дизайн, обновляя имеющийся процесс. Фактически при развертывании дизайна в существующий процесс происходит следующее:
Производится проверка возможности миграции незавершенных экземпляров процесса (другими словами, активных карточек) на новую схему процесса.
Создается новый каталог proc_<date_time>
, где date_time
- текущий момент времени развертывания процесса.
В механизме исполнения jBPM создается новое описание процесса с новым идентификатором, эквивалентным имени каталога развертывания. Этот новый идентификатор устанавливается в атрибуте jbpmProcessKey
объекта Proc
.
Производится миграция активных карточек.
Для обновления процесса в соответствии с измененным дизайном достаточно при развертывании последнего снять флажок Create new process и в выпадающем списке Existing process указать процесс, который необходимо обновить.
В данном разделе мы изменим экраны просмотра списка и редактирования сущности EBook
так, чтобы пользователи могли работать с ними в соответствии с бизнес-процессом.
Основная логика, связанная с процессом, реализуется в экране редактирования EBook
.
Начнем с доработки представления (view), с которым в экран загружается экземпляр EBook
. Вернитесь в Studio, найдите в секции Entities на панели навигатора представление eBook.edit
и откройте его на редактирование.
Выберите для поля Extends значение _local
, что означает, что текущее представление будет включать все нессылочные атрибуты сущности. Дополнительно включите атрибут proc
и установите для него в поле View значение start-process
.
Перейдем собственно к экрану. Найдите в секции Screens на панели навигатора экран ebook-edit.xml
и откройте его на редактирование. Перейдите на вкладку XML и полностью замените ее содержимое на следующий код:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://editCaption" class="com.sample.library.gui.ebook.EBookEdit" datasource="eBookDs" focusComponent="fieldGroup" messagesPack="com.sample.library.gui.ebook"> <dsContext> <datasource id="eBookDs" class="com.sample.library.entity.EBook" view="eBook.edit"/> <collectionDatasource id="attachmentsDs" class="com.haulmont.workflow.core.entity.CardAttachment" view="card-edit"> <query> <![CDATA[select a from wf$CardAttachment a where a.card.id = :ds$eBookDs order by a.createTs]]> </query> </collectionDatasource> </dsContext> <layout expand="windowActions" spacing="true"> <hbox spacing="true"> <fieldGroup id="fieldGroup" datasource="eBookDs" width="400px"> <field id="publication" width="100%"/> <field id="description" width="100%"/> </fieldGroup> </hbox> <groupBox caption="Process" orientation="horizontal" spacing="true" width="400px"> <label id="stateLabel" align="MIDDLE_LEFT"/> <hbox id="actionsBox" align="MIDDLE_RIGHT" spacing="true"/> </groupBox> <groupBox caption="Attachments" width="400px"> <table id="attachmentsTable" height="100px" width="100%"> <rows datasource="attachmentsDs"/> <columns> <column id="file"/> <column id="file.size"/> <column id="createTs"/> </columns> </table> </groupBox> <iframe id="windowActions" height="100%" screen="editWindowActions"/> </layout> </window>
Перейдите на вкладку Layout. Компоновка экрана станет следующей:
Рассмотрим добавленные элементы экрана.
Поле description
компонента fieldGroup
отображает значение атрибута description
карточки.
groupBox
с заголовком Process
содержит следующие элементы:
label
с идентификатором stateLabel
предназначен для отображения текущего состояния карточки.
Внутри контейнера hbox
с идентификатором actionsBox
мы будем программно создавать кнопки, позволяющие пользователю выбрать выход из назначения, другими словами - действия по процессу.
Таблица attachmentsTable
предназначена для отображения вложений, созданных оператором на этапе Scanning
процесса. Таблица соединена с источником данных attachmentsDs
.
Перейдите на вкладку Controller и замените ее содержимое на следующий код:
package com.sample.library.gui.ebook; import com.haulmont.cuba.core.entity.Entity; import com.haulmont.cuba.core.global.CommitContext; import com.haulmont.cuba.core.global.LoadContext; import com.haulmont.cuba.core.global.PersistenceHelper; import com.haulmont.cuba.gui.components.*; import com.haulmont.cuba.gui.data.DataSupplier; import com.haulmont.cuba.gui.data.DsContext; import com.haulmont.cuba.gui.export.ExportDisplay; import com.haulmont.cuba.gui.xml.layout.ComponentsFactory; import com.haulmont.workflow.core.app.WfService; import com.haulmont.workflow.core.entity.*; import com.haulmont.workflow.core.global.AssignmentInfo; import com.haulmont.workflow.core.global.WfConstants; import com.haulmont.workflow.gui.base.action.ProcessAction; import com.sample.library.entity.EBook; import javax.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; public class EBookEdit extends AbstractEditor<EBook> { @Inject protected WfService wfService; @Inject protected ComponentsFactory componentsFactory; @Inject protected BoxLayout actionsBox; @Inject protected DataSupplier dataSupplier; @Inject protected Label stateLabel; @Inject protected FieldGroup fieldGroup; @Inject protected Table attachmentsTable; @Inject protected ExportDisplay exportDisplay; @Override public void init(Map<String, Object> params) { } @Override protected void postInit() { EBook eBook = getItem(); if (PersistenceHelper.isNew(eBook)) { initProcess(eBook); } if (eBook.getState() == null) { stateLabel.setValue("State: not started"); } else { stateLabel.setValue("State: " + eBook.getLocState()); fieldGroup.setEditable(false); } initProcessActions(eBook); initAttachmentsTable(); } private void initProcess(final EBook eBook) { LoadContext loadContext = new LoadContext(Proc.class); loadContext.setQueryString("select p from wf$Proc p where p.code = :code") .setParameter("code", "book_scanning"); loadContext.setView("start-process"); Proc proc = dataSupplier.load(loadContext); if (proc != null) eBook.setProc(proc); else throw new IllegalStateException("Process not found"); eBook.setRoles(new ArrayList<CardRole>()); for (ProcRole procRole : proc.getRoles()) { if (procRole.getAssignToCreator()) continue; CardRole cardRole = new CardRole(); cardRole.setCard(eBook); cardRole.setProcRole(procRole); List<DefaultProcActor> defaultProcActors = procRole.getDefaultProcActors(); if (defaultProcActors.isEmpty()) throw new IllegalStateException("Default actor is not assigned for role " + procRole.getName()); cardRole.setUser(defaultProcActors.get(0).getUser()); eBook.getRoles().add(cardRole); } getDsContext().addListener(new DsContext.CommitListener() { @Override public void beforeCommit(CommitContext context) { context.getCommitInstances().addAll(eBook.getRoles()); } @Override public void afterCommit(CommitContext context, Set<Entity> result) { } }); } private void initProcessActions(EBook eBook) { AssignmentInfo assignmentInfo = wfService.getAssignmentInfo(eBook); if (eBook.getJbpmProcessId() == null && eBook.getState() == null) { addProcessAction(WfConstants.ACTION_START, assignmentInfo); } else if (assignmentInfo != null) { for (String actionName : assignmentInfo.getActions()) { addProcessAction(actionName, assignmentInfo); } } } private void addProcessAction(String actionName, AssignmentInfo assignmentInfo) { ProcessAction action = new ProcessAction(getItem(), actionName, assignmentInfo, this); Button button = componentsFactory.createComponent(Button.NAME); button.setAction(action); button.setAlignment(Alignment.MIDDLE_RIGHT); actionsBox.add(button); } private void initAttachmentsTable() { attachmentsTable.addGeneratedColumn("file", new Table.ColumnGenerator<CardAttachment>() { @Override public Component generateCell(final CardAttachment attachment) { LinkButton link = componentsFactory.createComponent(LinkButton.NAME); link.setCaption(attachment.getFile().getName()); link.setAction(new AbstractAction("") { @Override public void actionPerform(Component component) { exportDisplay.show(attachment.getFile()); } }); return link; } }); } }
Рассмотрим фрагменты кода контроллера.
Метод postInit()
вызывается после инициализации экрана и загрузки экземпляра EBook
с представлением, указанным в XML-дескрипторе (в данном случае - eBook.edit
).
После получения установленного в экране экземпляра EBook
производится проверка, новый ли это экземпляр, или загруженный из БД. В первом случае управление передается методу initProcess()
, который осуществляет подготовку карточки и экрана к старту нового экземпляра процесса:
protected void postInit() { EBook eBook = getItem(); if (PersistenceHelper.isNew(eBook)) { initProcess(eBook); }
Далее в зависимости от состояния карточки производится инициализация компонентов - stateLabel
отображает текущее состояние, а для fieldGroup
запрещается редактирование, если процесс уже стартовал:
protected void postInit() { ... if (eBook.getState() == null) { stateLabel.setValue("State: not started"); } else { stateLabel.setValue("State: " + eBook.getLocState()); fieldGroup.setEditable(false); }
Далее вызываются методы, производящие инициализацию возможных действий пользователя и таблицы вложений:
protected void postInit() { ... initProcessActions(eBook); initAttachmentsTable(); }
Рассмотрим метод initProcess()
.
В начале метода производится загрузка из базы данных экземпляра объекта Proc
с кодом book_scanning
, то есть созданного нами процесса. Если загрузка прошла успешно, то экземпляр Proc
устанавливается в карточке EBook
:
private void initProcess(final EBook eBook) { LoadContext loadContext = new LoadContext(Proc.class); loadContext.setQueryString("select p from wf$Proc p where p.code = :code") .setParameter("code", "book_scanning"); loadContext.setView("start-process"); Proc proc = dataSupplier.load(loadContext); if (proc != null) eBook.setProc(proc); else throw new IllegalStateException("Process not found");
Далее производится инициализация объектов CardRole
- исполнителей ролей для данной карточки. Инициализировать роли можно различными способами, в том числе интерактивно - например, позволяя создателю карточки самому выбрать исполнителей. Главное, чтобы на момент перехода процесса в какое-либо состояние типа Assignment роль, требуемая для этого этапа, была назначена. Для целей нашего примера исполнители заданы в объектах DefaultProcActor
на этапе настройки процесса, поэтому мы возьмем их оттуда и перенесем в объекты CardRole
:
private void initProcess(final EBook eBook) { ... eBook.setRoles(new ArrayList<CardRole>()); for (ProcRole procRole : proc.getRoles()) { if (procRole.getAssignToCreator()) continue; CardRole cardRole = new CardRole(); cardRole.setCard(eBook); cardRole.setProcRole(procRole); List<DefaultProcActor> defaultProcActors = procRole.getDefaultProcActors(); if (defaultProcActors.isEmpty()) throw new IllegalStateException("Default actor is not assigned for role " + procRole.getName()); cardRole.setUser(defaultProcActors.get(0).getUser()); eBook.getRoles().add(cardRole); }
В следующем фрагменте производится добавление всех созданных объектов CardRole
в CommitContext
перед коммитом экрана. Дело в том, что между Card
и CardRole
нет отношений каскадности сохранения, и если явно не сохранить созданные объекты CardRole
в той же транзакции, что и ссылающийся на них объект Card
, на Middleware возникнет ошибка. Обычно за включением в CommitContext
всех измененных экземпляров следят источники данных (datasources), однако в данном случае мы создаем и связываем объекты вручную, поэтому данный код необходим:
private void initProcess(final EBook eBook) { ... getDsContext().addListener(new DsContext.CommitListener() { @Override public void beforeCommit(CommitContext context) { context.getCommitInstances().addAll(eBook.getRoles()); } @Override public void afterCommit(CommitContext context, Set<Entity> result) { } }); }
Теперь рассмотрим методы инициализации кнопок, соответствующих возможным действиям пользователя по процессу, и таблицы вложений.
В методе initProcessActions()
для данной карточки загружаются данные о текущем назначении, и если таковое имеется для текущего пользователя, в методе addProcessAction()
создаются соответствующие кнопки:
private void initProcessActions(EBook eBook) { AssignmentInfo assignmentInfo = wfService.getAssignmentInfo(eBook); if (eBook.getJbpmProcessId() == null && eBook.getState() == null) { addProcessAction(WfConstants.ACTION_START, assignmentInfo); } else if (assignmentInfo != null) { for (String actionName : assignmentInfo.getActions()) { addProcessAction(actionName, assignmentInfo); } } } private void addProcessAction(String actionName, AssignmentInfo assignmentInfo) { ProcessAction action = new ProcessAction(getItem(), actionName, assignmentInfo, this); Button button = componentsFactory.createComponent(Button.NAME); button.setAction(action); button.setAlignment(Alignment.MIDDLE_RIGHT); actionsBox.add(button); }
Таблица вложений представляет собой обычный компонент Table
, связанный с источником данных attachmentsDs
, извлекающим экземпляры CardAttachment
данной карточки. Для загрузки файла вложения щелчком по имени файла в таблице создается генерируемая колонка для атрибута file
. В результате ячейки данной колонки отображают компонент LinkButton
, который по щелчку вызывает выгрузку соответствующего файла через интерфейс ExportDisplay
.
private void initAttachmentsTable() { attachmentsTable.addGeneratedColumn("file", new Table.ColumnGenerator<CardAttachment>() { @Override public Component generateCell(final CardAttachment attachment) { LinkButton link = componentsFactory.createComponent(LinkButton.NAME); link.setCaption(attachment.getFile().getName()); link.setAction(new AbstractAction("") { @Override public void actionPerform(Component component) { exportDisplay.show(attachment.getFile()); } }); return link; } }); }
Доработаем представления (view), с которым в экран загружается список экземпляров EBook
. Найдите в секции Entities на панели навигатора представление eBook.browse
и откройте его на редактирование. Включите атрибуты proc
, state
и description
. Для атрибута proc
установите в поле View значение _local
.
.
Найдите в секции Screens панели навигатора экран ebook-browse.xml
и откройте его на редактирование. Перейдите на вкладку XML и полностью замените ее содержимое на следующий код:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://browseCaption" class="com.sample.library.gui.ebook.EBookBrowse" lookupComponent="eBookTable" messagesPack="com.sample.library.gui.ebook"> <dsContext> <collectionDatasource id="eBookDs" class="com.sample.library.entity.EBook" view="eBook.browse"> <query> <![CDATA[select e from library$EBook e order by e.createTs]]> </query> </collectionDatasource> </dsContext> <layout expand="eBookTable" spacing="true"> <filter id="filter" datasource="eBookDs"> <properties include=".*"/> </filter> <table id="eBookTable" height="100%" width="100%"> <rows datasource="eBookDs"/> <columns> <column id="publication"/> <column id="description"/> <column id="locState"/> </columns> <rowsCount/> <actions> <action id="remove"/> </actions> <buttonsPanel id="buttonsPanel" alwaysVisible="true"> <button id="createBtn" action="eBookTable.create"/> <button id="editBtn" action="eBookTable.edit"/> <button id="removeBtn" action="eBookTable.remove"/> </buttonsPanel> </table> </layout> </window>
Отличия от стандартного экрана списка здесь следующие:
В список колонок таблицы eBookTable
добавлены description
и locState
- описание карточки и локализованное название текущего состояния.
Из списка декларативно создаваемых actions
таблицы eBookTable
исключены create
и edit
. При этом соответствующие кнопки на панели buttonsPanel
оставлены, потому что эти actions
мы создадим программно в коде контроллера.
Перейдите на вкладку Layout. Компоновка экрана станет следующей:
Перейдите на вкладку Controller и замените ее содержимое на следующий код:
package com.sample.library.gui.ebook; import java.util.Map; import com.haulmont.cuba.core.entity.Entity; import com.haulmont.cuba.gui.components.AbstractLookup; import com.haulmont.cuba.gui.components.Table; import com.haulmont.cuba.gui.components.actions.CreateAction; import com.haulmont.cuba.gui.components.actions.EditAction; import javax.inject.Inject; public class EBookBrowse extends AbstractLookup { @Inject protected Table eBookTable; @Override public void init(Map<String, Object> params) { eBookTable.addAction(new CreateAction(eBookTable) { @Override protected void afterCommit(Entity entity) { eBookTable.getDatasource().refresh(); } }); eBookTable.addAction(new EditAction(eBookTable) { @Override protected void afterCommit(Entity entity) { eBookTable.getDatasource().refresh(); } }); } }
Здесь в методе init()
в таблицу eBookTable
добавляются стандартные действия CreateAction
и EditAction
, но с переопределенным методом afterCommit()
, в котором производится перезагрузка источника данных таблицы. Это делается для того, чтобы отобразить в таблице измененное состояние карточки сразу после ее сохранения и передвижения по процессу.
Стандартные действия CreateAction
и EditAction
не производят перезагрузки источника данных после коммита открываемого экрана редактирования. Вместо этого они получают сохраненный экземпляр сущности с Middleware и просто устанавливают его в источнике данных вместо исходного.
В нашем же случае перезагрузка из БД необходима потому, что после сохранения редактируемой сущности EBook
запуск процесса производится в отдельной транзакции, и состояние карточки меняется как раз в этот момент. То есть после коммита экрана редактирования в источник данных возвращается экземпляр EBook
, в котором еще нет изменений, внесенных процессом. Для отображения этих изменений необходимо перечитать сущности из базы данных.
Запустите сервер admin
.
Откройте экран Description. На данном этапе экран редактирования выглядит следующим образом:
-> и создайте новую запись. Выберите издание книги и введите полное описание в полеТеперь можно либо просто сохранить карточку, нажав State (loc.) отобразится значение Approval
. Это означает, что карточка перешла в состояние ожидания одобрения менеджером.
Если теперь открыть карточку на редактирование, вы увидите, что все поля ввода запрещены, и никаких действий по процессу не доступно.
Выйдите из системы и войдите снова пользователем manager
. Откройте экран -> . Вы увидите поступившее вам назначение:
Нажав
, вы увидите экран редактирования карточки с возможностью одобрить или запретить сканирование книги:В этот же экран можно попасть обычным образом, выбрав строку в таблице экрана просмотра списка и нажав
.Нажмите manager
на данный момент больше нет назначений. Если же вы открывали экран редактирования из экрана списка EBook
, то вы увидите изменившееся состояние карточки:
Открыв карточку на редактирование вы опять не увидите возможности что-либо изменить или продолжить процесс, так как следующее действие должен выполнять пользователь operator
.
Выйдите из системы и войдите пользвателем operator
. Откройте экран -> . Вы увидите поступившее вам назначение:
Нажмите Success
и Fail
.
Предположим, вы как оператор выполнили сканирование книги. Нажмите
. Перед вами появится форма перехода, заданная в процессе:Нажмите Done
:
Открыв карточку на редактирование, можно увидеть состояние процесса и список вложений, добавленных оператором:
В данном разделе перечислены свойства приложения, имеющие отношение к подсистеме Workflow.
Путь для загрузки файла host-HTML визуального дизайнера относительно корневого URL веб-приложения.
Значение по умолчанию: wfdesigner/workflow/main.ftl
Интерфейс: WfConfig
Используется в блоке Web Client.