Interfejsy do Workflow Management

Wprowadzenie

Workflow opiera się na działaniach i zadaniach, które są przetwarzane w silniku Workflow. Działania mogą być tworzone zarówno w bazach danych OLTP, jak i w bazie repozytorium. Pierwsza część artykułu opisuje, w jaki sposób można tworzyć i przetwarzać działania z aplikacji za pomocą dostarczonych interfejsów Workflow. Przykłady kodu źródłowego dla poszczególnych punktów służą do wyjaśnienia kroków, które należy wykonać, aby osiągnąć pożądany cel.

Terminy, warunki i polecenia są używane w wielu miejscach Workflow do wyrażania złożonych relacji. Wszystkie te wyrażenia są częścią wspólnego języka skryptowego. Składnia języka skryptowego jest oparta na SQL, Pascal i Java. Język skryptowy można rozszerzyć o nowe polecenia i funkcje w dowolnym momencie. Zwłaszcza gdy konieczne jest sprawdzanie złożonych warunków, można napotkać ograniczenia języka skryptowego pod względem jego możliwości wyrażania logiki lub wydajności. W takim przypadku problemy te można rozwiązać za pomocą specjalnych funkcji. W drugiej części artykułu wyjaśniono, w jaki sposób język skryptowy może zostać rozszerzony o nowe funkcje.

Grupa docelowa

  • Deweloperzy

Definicje terminów

ActivityContainer – to wielokrotnego użytku kontener przeznaczony dla aktywności. Udostępnia on wszystkie podstawowe metody potrzebne do obsługi (przetwarzania) aktywności.

Na diagramie, w celu zachowania przejrzystości, przedstawiono jedynie metody typu getter. Z poziomu ActivityContainer można uzyskać dostęp do Workitems, SeriesElements, Attachments oraz Performer.

Diagram klas ActivityContainer i zależnych od niego obiektów.

Workitem – jeśli dana aktywność jest typu STANDARD lub SERIES_ELEMENT, może posiadać przypisane zadania. Interfejs Workitem zapewnia niemodyfikowalny widok zadania — oznacza to, że pozwala jedynie na odczyt danych, bez możliwości ich zmiany. Za pomocą tego interfejsu można uzyskać informacje o statusie zadania, osobie realizującej oraz momentach zmiany statusu (te elementy nie są pokazane na diagramie).

SeriesElement – jeśli aktywność jest typu SERIES, może w określonych momentach czasowych generować kolejne aktywności. Interfejs SeriesElement udostępnia ograniczony (uproszczony) widok aktywności. Dane z tego interfejsu mogą być wykorzystywane do prezentacji elementów serii w listach lub tabelach. Za pomocą zawartych w nim kluczy można ponownie wczytać odpowiednie aktywności, korzystając z CisWorkflowManager oraz ActivityContainer. Atrybuty interfejsu SeriesElement są niemodyfikowalne – służą wyłącznie do odczytu.

Attachment – do jednej aktywności można dołączyć dowolną liczbę powiązań (załączników).
Celem takiego powiązania (Attachment) może być dowolny obiekt biznesowy (Business Entity).
Jedynym warunkiem jest to, że dany obiekt musi być utrwalony w tej samej bazie danych, w której znajduje się aktywność.

Performer – określa, kto otrzymuje zadania wynikające z danej aktywności. Może to być pojedyncza osoba lub grupa osób. Każda osoba z tej grupy otrzymuje osobne zadanie przypisane do tej samej aktywności.

Instrukcja

CisWorkflowManager i ActivityContainer

Dwie klasy CisWorkflowManager i ActivityContainer stanowią centralny punkt wejścia do programowania Workflow.

ActivityContainer reprezentuje widok aktywności i zależnych od niej obiektów. I może być ponownie użyty, tzn. ActivityContainer, który został utworzony raz, może być użyty dla kilku aktywności z rzędu.

Podczas gdy ActivityContainer służy do przetwarzania aktywności, CisWorkflowManager jest odpowiedzialny za Persistence service. CisWorkflowManager może być używany do tworzenia, ładowania, zapisywania itp. aktywności.

Metody ustawiania i odczytywania identyfikatora GUID bazy danych oraz kodu

ActivityContainer zawiera metody ustawiania i odczytywania UserData baseGuid i UserCode, a także metody odczytywania DatabaseGuid i kodu.

Metody z User są używane dla wszystkich operacji Persistence service. Aby załadować aktywność, należy wywołać metody setUserDatabaseGuid(…) i setUserCode(…). Metoda ładowania aktywności musi być następnie wywołana w CisWorkflowContainer.

Dwie metody getDatabaseGuid() i getCode() zawsze zawierają odpowiednie dane bieżącej aktywności ActivityContainer, podczas gdy dwie metody getUserDatabaseGuid() i getUserCode() mogą również przyjmować inne wartości, np. w celu późniejszego załadowania aktywności.

Zachowanie transakcyjne

CisWorkflowManager umożliwia integrację kontenera ActivityContainer z istniejącą transakcją lub użycie go w oddzielnej transakcji najwyższego poziomu.

Decyzja ta jest podejmowana w momencie tworzenia kontenera ActivityContainer. Nie ma możliwości zmiany tego zachowania w późniejszym czasie.

Włączenie do istniejącej transakcji

Jeśli ActivityContainer ma zostać zintegrowany z istniejącą transakcją, ActivityContainer musi zostać utworzony przy użyciu metody createEmptyActivityContainer() w CisWorkflowManager.

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = wm.createEmptyActivityContainer();

Podczas integracji z istniejącymi transakcjami ważne jest, aby otaczająca (nadrzędna) transakcja korzystała z tej samej bazy danych co ActivityContainer.

CisEnvironment env = CisEnvironment.getInstance();

CisTransactionManager tm = env.getTransactionManager();

byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);

ac.setUserDatabaseGuid(dbGuid);

ActivityContainer można następnie wywołać w ramach oddzielnej transakcji w następujący sposób.

byte[] transactionGuid = tm.beginNew(CisTransactionManager.OLTP);

try {


CisWorkflowManager wm = CisWorkflowManager.getInstance();

wm.update(ac);


tm.commit(transactionGuid);

} catch …

Użycie własnej transakcji najwyższego poziomu

Jeśli ActivityContainer nie musi być trwały z innymi obiektami w tej samej transakcji, można użyć oddzielnej transakcji najwyższego poziomu. Wszystkie transakcje są następnie przeprowadzane w ramach ActivityContainer lub CisWorkflowManager. Aby uzyskać taki ActivityContainer, należy wywołać metodę createEmptyStandaloneActivityContainer() w CisWorkflowManager. Podobnie jak w przypadku innych ActivityContainer, identyfikator GUID bazy danych można odpowiednio ustawić.

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = wm.createEmptyStandaloneActivityContainer();

byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);

ac.setUserDatabaseGuid(dbGuid);

Tworzenie aktywności

Aby utworzyć aktywność, należy utworzyć ActivityContainer, zdefiniować bazę danych i wywołać odpowiednią metodę w CisWorkflow Manager.

CisEnvironment env = CisEnvironment.getInstance();

CisTransactionManager tm = env.getTransactionManager();

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = wm.createEmptyStandaloneActivityContainer();

byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);

ac.setUserDatabaseGuid(dbGuid);

wm.create(ac);

Ładowanie aktywności

Aby załadować aktywność do kontenera ActivityContainer, należy ustawić identyfikator GUID bazy danych i kod żądanej aktywności w kontenerze w metodach setUserDatabaseGuid i setUserCode. Następnie należy wywołać metodę load(…) w CisWorkflowManager.

CisEnvironment env = CisEnvironment.getInstance();

CisTransactionManager tm = env.getTransactionManager();

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = wm.createEmptyStandaloneActivityContainer();

byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);

String code = “…”;

ac.setUserDatabaseGuid(dbGuid);

ac.setUserCode(code);

wm.load(activity);

Zapisywanie aktywności

Aby zapisać aktywność, należy wywołać metodę update(…) w CisWorkflow Manager. Następnie należy wywołać metodę reload(..) w celu dostarczenia do kontenera aktualnych danych. Ważne jest, aby wywołać tę metodę, ponieważ na przykład elementy robocze zostały utworzone po zapisaniu, które stają się widoczne tylko w ActivityContainer poprzez metodę reload(…).

Jeśli kontener jest zawarty w innej transakcji, funkcja reload(…) może zostać wywołana dopiero po zatwierdzeniu.

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = ...; wm.update(ac); wm.reload(ac);

Usuwanie aktywności

Aktywności nie można usunąć. Zamiast tego status aktywności można ustawić na CANCELED lub DONE.

Duplikowanie aktywności

Podczas powielania aktywności powielane są tylko dane wymagane do utworzenia nowej aktywności. Status, zadania itp. nie są kopiowane. Jeśli powiedzie się, zduplikowana aktywność znajduje się w przeniesionym ActivityContainer.

CisWorkflowManager wm = CisWorkflowManager.getInstance();

ActivityContainer ac = ...;

boolean successfull = wm.duplicate(ac);

Tworzenie kopii obiektu ActivityContainer

W przypadku, gdy wymagana jest dokładna kopia ActivityContainer, istnieje metoda getCopy(). Kopia nie zawiera jednak tekstów, które można sprawdzić za pomocą getText(…).

Flagi

Znajomość stanu ActivityContainer jest ważna dla programowania interfejsu, zwłaszcza dla wyświetlania trybu aplikacji.

Poniższa lista przedstawia metody dostępne w ActivityContainer.

  • isPersistent()
  • isValid()
  • isChanged()
  • isReadOnly()
  • isStateChangeAllowed()
  • isDatabaseGuidChanged()
  • isCodeChanged()

Seria

Serie są specjalną formą aktywności. W przeciwieństwie do aktywności, serie nie mają elementów roboczych. Zamiast tego, serie generują inne aktywności w określonych momentach ich życia. Aktywności te można sprawdzać za pomocą metody getSeriesElements().

Tworzenie serii

Aby utworzyć serię, należy najpierw utworzyć aktywność analogiczną do znanego wzorca.

CisEnvironment env = CisEnvironment.getInstance();


CisTransactionManager tm = env.getTransactionManager();


CisWorkflowManager wm = CisWorkflowManager.getInstance();


ActivityContainer ac = wm.createEmptyStandaloneActivityContainer();

byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);

ac.setUserDatabaseGuid(dbGuid);


wm.create(ac);

…

Aby przekształcić „normalną” aktywność w serię, należy zmienić typ na SERIES.

Czas rozpoczęcia serii zawsze reprezentuje następny punkt w czasie, w którym tworzona jest aktywność. Czas ten zmienia się po każdej utworzonej aktywności.

Nowo utworzona aktywność otrzymuje:

  • jako czas rozpoczęcia – bieżący czas rozpoczęcia serii,

  • jako czas zakończenia – wartość obliczoną przez dodanie parametru seriesDuration do czasu rozpoczęcia.

Interwał serii i powiązany kalendarz są używane do obliczenia następnego czasu, w którym aktywność ma zostać wygenerowana.

ac.setType(ActivityType.SERIES);

ac.setStartTime(...);

ac.setEndTime(...);

ac.setSeriesDuration(...);

ac.setSeriesInterval(...);

ac.setSeriesIntervalCalendar(...);

...

wm.update(ac);

...

Interwał serii i kalendarz

Interwał serii to symboliczny czas np:

  • W drugi wtorek każdego miesiąca o 17:15
  • W każdy poniedziałek i wtorek o 15:00

Godziny te zawsze zależą od kalendarza, na którym mają być oparte obliczenia. Na przykład 17:15 to inna godzina w Europie niż w Azji.

Pole SymbolicIntervalField jest dostępne do wprowadzania czasu symbolicznego w programowaniu interfejsu. Zawiera ono wszystkie możliwe warianty czasów symbolicznych w wyskakującym okienku i posiada regulowany kalendarz. Pole ma dwie metody getValue() i getCisCalendar(), aby uzyskać wymagane wartości do ustawienia serii.

SymbolicIntervalField field = new SymbolicIntervalField(...); 
... 
ac.setSeriesInterval(field.getValue()); 
ac.setSeriesIntervalCalendar(field.getCisCalendar()); ...

Linki (Attachments)

Dla każdej aktywności można utworzyć dowolną liczbę linków do innych obiektów CisObjects. Warunkiem utworzenia łącza jest to, że aktywność i obiekty CisObjects znajdują się w tej samej bazie danych. Łącza są zapisywane wraz z aktywnością w formie załączników.

Attachment attachment = ac.createAttachment();

attachment.setCisObject(aCisObject);

attachment.setDescription(“…”);

ac.getAttachments().add(attachment);

SequenceNumber jest przypisywany na podstawie kolejności na liście podczas zapisywania, zaczynając od zera i rosnąco. Pierwszy załącznik na liście, tj. z numerem SequenceNumber zero, m a szczególne znaczenie dla uruchomienia aplikacji dla zadania.

Ustawianie aplikacji dla zadań

Zadania aktywności są przetwarzane za pomocą aplikacji dostępnych w systemie. W aktywności można określić, która aplikacja ma zostać otwarta.

Można to zrobić bezpośrednio, określając aplikację, lub pośrednio za pomocą obiektów biznesowych, system na ich podstawie określa, jaka aplikacja powinna zostać otwarta.

Bezpośrednie ustawienie aplikacji

Każda aplikacja w systemie może być jednoznacznie identyfikowana za pomocą identyfikatora GUID. Ten identyfikator GUID musi być ustawiony w ActivityContainer. Można również określić, które ActionId i parametry są przekazywane do aplikacji. Możliwe ActionIds i parametry są zdefiniowane w obiekcie programistycznym Application dla danej aplikacji.

W poniższym przykładzie bieżąca aplikacja jest zdefiniowana jako aplikacja dla aktywności.

Przykład

CisApplicationManager am = env.getApplicationManager();

…

byte[] applicationGuid = am.getApplicationGuid();

CisParameterList actionParameters = new CisParameterList();

…

ac.setApplicationGuid(applicationGuid);

ac.setActionId((short) 1);

ac.setActionParameters(actionParameters);

Ustawienie aplikacji w sposób pośredni

Jeśli żadna aplikacja nie jest ustawiona bezpośrednio, linki są używane do określenia aplikacji. Używany jest link, który ma zero jako SequenceNumber. Domyślna aplikacja jest określana i uruchamiana dla obiektu, który zawiera. Sam obiekt jest parametrem transferu.

Realizatorzy zadań

Dla każdej aktywności, z wyjątkiem aktywności typu SERIES, w momencie przejścia w status WAITING tworzone są zadania. Dla każdego realizatora powstaje osobne zadanie. Sposób ustalania realizatorów zależy od konfiguracji w Performerze.

Performer określa, w jaki sposób mają zostać znalezieni realizatorzy zadań. Mogą oni pochodzić z relacji partnerskich powiązanych ze stanowiskiem, organizacją lub osobą. W przypadku partnera w systemie zapisane jest, który użytkownik (User) jest z nim związany. Każdemu znalezionemu użytkownikowi przypisywane jest osobne zadanie.

Oprócz określania realizatora poprzez partnera można również wskazać użytkownika bezpośrednio. Jeśli zadanie ma zostać przypisane do kilku użytkowników, można ich zgrupować w role workflowowe.

Jeśli do ustalenia użytkowników potrzebne są dodatkowe powiązania lub funkcje, można w Performerze zdefiniować odpowiednie wyrażenie. Umożliwia to na przykład przypisanie zadania przełożonemu, gdy wartość zamówienia przekracza określony próg.

Przykład
Aktualny użytkownik zostaje ustawiony jako realizator zadania.

Performer p = ac.getPerformer();

p.setType(PerformerType.USER);

p.setGuid(env.getUserGuid());

Jeśli istnieje więcej niż jeden użytkownik, tj. utworzono więcej niż jedno zadanie, ważne jest, aby rozróżnić, czy wszystkie zadania muszą zostać przetworzone, czy tylko jedno z nich, aby zakończyć działanie. Metoda setAllocation(…) może być użyta do ustawienia pożądanego zachowania.

Przykład
W poniższym przykładzie wszyscy użytkownicy muszą ukończyć swoje zadania, aby działanie mogło zostać zakończone.

Performer p = ac.getPerformer();

p.setType(PerformerType.ROLE);

p.setAllocation(PerformerAllocation.ALL);

p.setGuid(env.getUserGuid());

Realizator przy przekroczeniu czasu wykonania

Jeśli czas realizacji aktywności zostanie przekroczony, system reaguje w zależności od ustawionego typu eskalacji. To zachowanie można określić za pomocą metody setEscalationType w obiekcie ActivityContainer.

Jeżeli zostanie ustawiona wartość FORWARD, to po przekroczeniu dopuszczalnego czasu wszystkie istniejące zadania zostaną odebrane ich dotychczasowym użytkownikom, a w ich miejsce zostaną utworzone nowe zadania przypisane do innych użytkowników.

Nowych użytkowników, którzy mają przejąć zadania po eskalacji, definiuje się w obiekcie EscalationPerformer.

ac.setEscalationType(EscalationType.FORWARD);

 

Performer ep = ac.getEscalationPerformer();

ep.setType(PerformerType.USER);

ep.setAllocation(PerformerAllocation.ALL);

ep.setGuid(env.getUserGuid());

Warunki

Warunki mogą być definiowane w aktywnościach. Jeśli warunek zostanie przekazany do metody setCondition(…), Workflow Engine sprawdza, czy warunek jest spełniony przy każdej zmianie statusu. Jeśli nie jest on już spełniony, aktywność, a w konsekwencji wszystkie zadania, są ustawiane na CANCELED.

Podobnie można ustawić warunek dla serii. W tym celu dostępna jest metoda setSeriesCondition(…). Jeśli ten warunek nie jest już spełniony, nie są generowane dalsze działania.

Opis składni warunków można znaleźć w artykule dotyczącym Silnik workflow.

Zmiana stanu aktywności

Status aktywności można ręcznie zmienić na DONE lub CANCELED. Takie działanie stosuje się na przykład wtedy, gdy aktywność została utworzona omyłkowo. Zmiana statusu oznacza, że wszystkie zadania zostaną również zmienione po zapisaniu działania.

ac.setState(ActivityState.DONE)

ac.setState(ActivityState.CANCELED)

Klasyfikacje

W celu ograniczenia liczby aktywności znalezionych w wyszukiwarkach i aplikacjach zapytań, ActivityContainer posiada kilka metod klasyfikacji aktywności. SourceType wskazuje pochodzenie aktywności. W większości przypadków są to tak zwane działania „ad hoc”.

ac.setGroup1(…);

ac.setGroup2(…);

ac.setGroup3(…);

ac.setHierarchy1(…);

ac.setHierarchy2(…);

ac.setActivityClass(…);

ac.setSourceType(…);

Pozostałe informacje

Confirmation

Metoda setConfirmation(…) służy do określenia, czy przetwarzanie zadania musi zostać potwierdzone. W tym celu w systemie należy kliknąć przycisk na pasku narzędzi Workflow. Jeśli potwierdzenie nie jest wymagane, zadanie zmienia status na Completed zaraz po jego otwarciu.

Workitem

Listę zadań aktywności można uzyskać za pomocą metody getWorkitems(). Zawarte w niej elementy robocze nie mogą zostać zmienione.

WorkState

Jeśli podczas przetwarzania aktywności wystąpią błędy w silniku Workflow, aktywność zostanie zatrzymana, tj. wykluczona z dalszego przetwarzania. Status działania można sprawdzić za pomocą funkcji getWorkState().

CrmType

CrmType jest wymagany tylko dla zarządzania relacjami. Wszystkie działania należące do elementu zarządzania relacjami można określić, podając identyfikator GUID w CrmType.

DefinitionGuid

Jeśli aktywność jest oparta na definicji aktywności, identyfikator GUID tej definicji aktywności można sprawdzić za pomocą metody getDefinitionGuid().

SeriesActivityGuid

Jeśli aktywność jest oparta na serii, identyfikator GUID serii można uzyskać za pomocą metody getSeriesActivityGuid().

Priorytet

Działanie może mieć priorytet od 1 (wysoki) do 9 (niski).

Tłumaczenia

Aktywności w systemie mają elementy, które można przetłumaczyć: oznaczenia i opis.

Opis można sprawdzić za pomocą funkcji getDescription() w ActivityContainer, a powiązane dane NLS za pomocą funkcji getDescriptionNLSData().

Oprócz nazwy można wprowadzić opis w kilku językach. Opis ten może być używany na przykład jako szczegółowy opis zadania.

Sring text = ac.getText("en"); 
ac.setText("en", "text...");

Klasy i interfejsy

Wszystkie klasy i interfejsy wymagane do rozszerzenia języka skryptowego można znaleźć w pakiecie com.cisag.pgm.util.

Następujące klasy/interfejsy są interesujące dla tego rozszerzenia:

  • ParserLogic
  • ParserEnvironment
  • ParserExpression
  • ParserFunction
  • ParserResult
  • ParserType
ParserLogic

Klasa ParserLogic udostępnia dla warunków, wyrażeń i sekwencji poleceń oddzielne metody, które umożliwiają utworzenie drzewa parsowania na podstawie przekazanego ciągu znaków.
Utworzone drzewo składa się z obiektów typu ParserExpression, a wynikiem działania metody jest element nadrzędny (korzeń) tego drzewa.

Dodatkowo klasa ParserLogic pełni rolę pamięci podręcznej (cache) dla utworzonych drzew parsowania, dzięki czemu ciągi znaków, które zostały już wcześniej przetworzone, nie muszą być ponownie parsowane, co zwiększa wydajność działania systemu.

public ParserExpression getStatement(String statement) 
throws ParserException {...}

public ParserExpression getCondition(String condition) 
throws ParserException {...}

public ParserExpression getExpression(String expression) 
throws ParserException {...}
ParserEnvironment

Klasa ParserEnvironment reprezentuje środowisko uruchomieniowe dla wyrażeń ParserExpression. Wyrażenie ParserExpression może zostać przekazane do metody evaluate(…). Jest ono oceniane, a wynik jest zwracany jako ParserResult.

public ParserResult evaluate(ParserExpression expression) 
throws ParserException {...}
ParserExpression

Klasa ParserExpression reprezentuje abstrakcyjną nadklasę funkcji. Metoda evaluate(…) zawiera rzeczywistą funkcję. Jako parametr wejściowy przekazywana jest tablica z wynikami ParserResults. Wartością zwracaną jest pojedynczy ParserResult. Metoda jest wywoływana dla wszystkich możliwych sygnatur, tzn. musi być rozpoznane w metodzie, która sygnatura została użyta do wywołania.

ParserExpressions mogą być używane w OLTP i sesjach. Klasa ParserExpression nie może zatem mieć żadnych zmiennych instancji specyficznych dla sesji lub OLTP, tj. w szczególności żadnych menedżerów z com.cisag.pgm.appserver i żadnych instancji klas logicznych.

ParserFunction

Klasa ParserFunction pełni rolę fabryki oraz abstrakcyjnej klasy bazowej dla wszystkich fabryk funkcji parsera. Aby utworzyć jedną lub więcej własnych funkcji, należy odziedziczyć tę klasę. Należy zaimplementować metody:

  • getNames() – zwraca nazwy wszystkich funkcji, które można utworzyć w tej fabryce.
  • getExpression(…) – zwraca żądaną funkcję (ParserExpression) dla nazwy funkcji.

Należy zauważyć, że metoda getExpression(…) jest wywoływana tylko raz dla każdej nazwy funkcji. Sama wygenerowana funkcja zapewnia metodę umożliwiającą generowanie kolejnych kopii funkcji, jeśli jest to wymagane.

ParserResult

Klasa ParserResult może być zarówno parametrem, jak i wynikiem funkcji. Typ danych obiektu w ParserResult jest określony przez ParserType.

Zawartość ParserResult nie może zostać zmieniona, dostępne są tylko odpowiednie metody getter. Aby utworzyć ParserResult, należy wywołać odpowiednią statyczną metodę create na ParserResult, np. createBoolean(boolean value).

ParserType

ParserType zapewnia metadane dla ParserResult. Niektóre typy ParserType, takie jak STRING, GUID, NUMBER, są już wstępnie zdefiniowane w samej klasie. Można jednak również tworzyć własne typy, np. w oparciu o CisObject.

Zakres ważności funkcji

Aby funkcje działały poprawnie, potrzebują odpowiedniego środowiska, tzn. muszą być spełnione określone warunki brzegowe. Rozróżnia się dwa główne aspekty: po pierwsze bazę danych, a po drugie to, czy funkcja wymaga aktywności, czy nie.

Jeśli funkcja ma być użyta np. w warunku przejścia (Transition) w definicji aktywności, nie może ona odwoływać się do aktywności, ponieważ ta zostanie utworzona dopiero w późniejszym etapie.

Zakres ważności funkcji określa się w konstruktorze funkcji. W klasie ParserFunctions zdefiniowane są możliwe zakresy ważności w postaci stałych.

  • WF_ACTIVITY_REPOSITORY
  • WF_ACTIVITY_OLTP
  • WF_TRANSITION_OLTP
  • WF_TRANSITION_REPOSITORY

Jeśli wykonanie funkcji zależy tylko od bazy danych, można również użyć jednej z dwóch podsumowanych stałych.

  • WF_REPOSITORY
  • WF_OLTP

Jeśli funkcja może być używana niezależnie od bazy danych i aktywności, można również użyć następującej stałej:

  • WF_ALL

Rejestrowanie funkcji

Nowo utworzone funkcje muszą zostać zarejestrowane w klasie CisInitaliser w pakiecie com.cisag.app.general.log przy użyciu metody initParserFunctions(…).

public static void initParserFunctions(

ParserFunctionProvider functionProvider) {

functionProvider.addFunction(new WorkflowFunctions());

functionProvider.addFunction(new BookFunctions());

}

Rozszerzanie języka skryptowego

Przykład zostanie wykorzystany do wyjaśnienia, w jaki sposób język skryptowy może zostać rozszerzony o wyrażenie loadBook.

Tworzenie fabryki

Tworzenie klasy

Obiekt biznesowy Book znajduje się w pakiecie com.cisag.app.edu.obj. Nowa funkcja jest zdefiniowana w następujący sposób:

package com.cisag.app.edu.log;

 

import com.cisag.app.edu.obj.Book;

import com.cisag.pgm.util.ParserFunction;

import … // Weitere Imports

 

public class BookFunctions extends ParserFunction {

….

}
Definiowanie nazwy metody i możliwych sygnatur

W wygenerowanej klasie zdefiniowane są dwie stałe. Stała NAMES zawiera tablicę łańcuchów z listą nazw funkcji. W tym przypadku jest to funkcja loadBook.

Możliwe sygnatury dla funkcji są przechowywane w stałej SIGNATURE_LOAD. Sygnatura może nie zawierać żadnego, jednego lub wielu parametrów. Każdy parametr ma nazwę i typ. Funkcja loadBook powinna być wywoływana przy użyciu parametru guid typu GUID lub number typu STRING.

Nazwy stałych NAMES i SIGNATURE_LOAD są dowolne.

private final static String[] NAMES = new String[]{“loadBook”};

 

public final static ParserExpression.Signature[]

SIGNATURE_LOAD = new ParserExpression.Signature[] {

ParserExpression.signature(“guid”, ParserType.GUID),

ParserExpression.signature(“number”, ParserType.STRING),

};
Tworzenie konstruktora

Obszary, dla których ta funkcja jest dozwolona, są zdefiniowane w konstruktorze. Niezbędne stałe można znaleźć w klasie ParserFunctions.

Metoda setFlag może być używana do włączania lub wyłączania obszaru. Funkcja „loadBook” jest dozwolona tylko dla baz danych OLTP.

public BookFunctions() {

setFlag(ParserFunction.WF_OLTP, true);

}
Pozostałe funkcje

Klasa BookFunctions zawiera dwie kolejne metody do implementacji: getNames() i getExpression(String)

Metoda getNames() zwraca nazwy wszystkich funkcji zdefiniowanych w klasie. W tym przykładzie stała NAMES zawiera tylko wartość loadBook.

public String[] getNames() {

return NAMES;

}

Metoda getExpression(String) jest wywoływana tylko raz. Dla każdej przekazanej nazwy funkcji tworzy ona instancję klasy, w której funkcja jest zaimplementowana. Wygenerowana klasa jest używana jako prototyp do późniejszego tworzenia kolejnych instancji.

public ParserExpression getExpression(String name) {

if (name.equals(NAMES[0])) {

return new LoadExpression(0, 0);

} else {

throw new RuntimeException(“unkown function”);

}

}

Tworzenie funkcji

Funkcja loadBook jest zaimplementowana w klasie LoadExpresion, która została wbudowana w kod źródłowy jako klasa wewnętrzna.

Klasa LoadExpression dziedziczy z ParserExpression i dlatego musi implementować dwie abstrakcyjne metody, evaluate(…) i create(…). Metoda getShortName() jest również nadpisana i zwraca nazwę funkcji.

public static class LoadExpression extends ParserExpression {

public LoadExpression(int line, int col) {

... }

protected ParserResult evaluate(ParserResult[] parameters) throws ParserException { ... }

public ParserExpression create(int line, int col) {

... }

public String getShortName() { ...

}

}
Konstruktor

Konstruktor jest wywoływany z dwiema wartościami int dla wiersza i kolumny. Te dwie wartości określają pozycję w tekście do przeanalizowania i są używane do komunikatów o błędach.

Podpisy (guid i numer) oraz wartość zwracana są ustawiane w konstruktorze. W tym przypadku wartość zwracana powinna zawierać instancję klasy Book.

public LoadExpression(int line, int col) {

super(line, col, TYPE_FUNCTION);

setSignatures(SIGNATURE_LOAD);

setResultType(ParserType.createCisObject(

CisObjectUtility.getClassGuid(Book.class)));

}
Metoda evaluate(…)

Funkcja jest wykonywana w ramach tej metody. Najpierw sprawdza, czy jako parametr przekazano wartość null. Jeśli tak jest, wynikiem funkcji jest również null lub ParserResult.NULL. To zachowanie powinno być obecne we wszystkich funkcjach.

Następnie podejmowana jest próba wygenerowania klucza dla żądanego obiektu. Możliwe sygnatury są analizowane. Jeśli parametr jest typu ParserResult.STRING, klucz jest generowany przy użyciu metody buildByNumberKey(…); w przypadku ParserResult.GUID używana jest metoda buildPrimaryKey(…). Jeśli nie można ocenić sygnatury lub nie można określić klucza, należy wyświetlić odpowiednie komunikaty o błędach.

Po określeniu klucza obiekt jest ładowany zgodnie ze znanym schematem i pakowany jako ParserResult.

protected ParserResult evaluate(ParserResult[] parameters)

throws ParserException {

 

// retrieve parameter

ParserResult keyValue = parameters[0];

if (keyValue.isNull()) {

return ParserResult.NULL;

}

 

// build key

byte[] key = null;

switch (keyValue.getDatatype()) {

case ParserResult.STRING:

key = Book.buildByNumberKey(keyValue.getString());

break;

case ParserResult.GUID:

key = Book.buildPrimaryKey(keyValue.getGuid());

break;

default:

errorMessage(“invalid argument type”);

}

if (key == null) {

errorMessage(“internal error”);

return ParserResult.NULL;

}

 

// load object

CisEnvironment        env = CisEnvironment.getInstance();

CisObjectManager      om = env.getObjectManager();

CisTransactionManager tm = env.getTransactionManager();

 

byte[] transactionGuid = tm.beginNew();

try {

CisObject result = om.getObject(key);

 

if (result != null) {

return ParserResult.createCisObject(

(CisObject)result.clone());

} else {

return ParserResult.NULL;

}

}

finally {

tm.rollback(transactionGuid);

}

}
Metoda create(…)

Metoda create(…) służy do tworzenia nowej instancji klasy ParserExpression. Po tym, jak metoda getExpression(…) utworzy prototyp obiektu ParserExpression, wszystkie kolejne instancje tej klasy są tworzone właśnie poprzez wywołanie metody create(…) na tym prototypie.

Ważne jest, aby tworzenie nowej klasy odbywało się z wykorzystaniem mechanizmu refleksji (Classreflection), ponieważ pozwala to uwzględnić ewentualne podmiany lub rozszerzenia klas.

Przy takich podmianach należy pamiętać, że nowa klasa musi posiadać ten sam zestaw parametrów w konstruktorze, co klasa oryginalna, aby mogła być poprawnie utworzona przez mechanizm refleksji.

public ParserExpression create(int line, int col) {

// must use class reflection to use

// the correct class loader

try {

Class loadExpression = CisClass.forName(

“com.cisag.app.edu.log.BookFunctions” +

“$LoadExpression”);

Constructor constuctor = loadExpression.getConstructor(

new Class[] {Integer.TYPE,Integer.TYPE});

return (ParserExpression)constuctor.newInstance(

new Object[] {new Integer(line),

new Integer(col)});

} catch (Exception ex) {

throw new CisRuntimeException(ex);

}

}
Metoda getShortName()

Metoda zwraca nazwę funkcji, w tym przypadku loadBook.

public String getShortName() {

return NAMES[0];

}

Zdarzenia

System rozróżnia dwa rodzaje zdarzeń: zdarzenia jednostek biznesowych i zdarzenia zaprogramowane. Wyzwalanie zaprogramowanego zdarzenia jest wyraźnie zakodowane w logice programu aplikacji. Zdarzenia jednostki biznesowej są wyzwalane niejawnie, gdy obiekt biznesowy jest zmieniany przez usługę trwałości.

Podczas tworzenia definicji aktywności definiowane są zdarzenia, dla których mają one generować działania. Gdy zdarzenie jest wyzwalane, Workflow Engine sprawdza, czy definicja aktywności powinna zareagować na zdarzenie i powiadamia odpowiednie definicje aktywności.

Uruchamianie zdarzeń

Zdarzenia programowane są definiowane w aplikacji Obiekty deweloperskie jako zdarzenia obiektu deweloperskiego.
W definicji takiego zdarzenia określa się:

  • jakie parametry ma ono przyjmować

  • w której bazie danych ma być uruchamiane

  • oraz czy mają istnieć podtypy zdarzenia

Deweloperzy aplikacji definiują w kodzie, kiedy i w jakich warunkach dane zdarzenia mają zostać wywołane.
Do ich uruchamiania służy interfejs com.cisag.pgm.appserver.CisSystemManager, który udostępnia metodę fireEvent w kilku wariantach – z różnymi zestawami parametrów.

Za pomocą tej metody uruchamiane jest zdarzenie, które następnie jest przetwarzane przez silnik Workflow.

Minimalny zestaw parametrów wymaganych przez metodę obejmuje:

  • nazwę zdarzenia (nazwa obiektu deweloperskiego typu Zdarzenia),

  • listę parametrów przekazywanych do zdarzenia.

Wywołanie wygląda nastepujaco:

fireEvent(String name, CisParameterList parameters).

fireEvent(String name, CisParameterList parameters)

Nazwa jest w pełni kwalifikowaną nazwą zdarzenia obiektu deweloperskiego, np. com.cisag.app.internal.log.SupportRequestNotification.

Parametr przekazywany do metody, parameters, musi zawierać wszystkie parametry, które zostały zdefiniowane w obiekcie deweloperskim typu zdarzenia. Lista parametrów musi zawierać wszystkie parametry zdefiniowane w obiekcie deweloperskim. Jeśli w zdarzeniu obiektu deweloperskiego zdefiniowano więcej parametrów niż jest dostępnych na liście parametrów przekazywanej do metody, zostanie zgłoszony wyjątek. W zdarzeniu obiektu deweloperskiego można zdefiniować dwa typy parametrów: parametry typu Business Object i typu logicznego typu danych. Parametry typu

Business Object są przekazywane do CisParameterList za pomocą metody

setObject(String name, Object value)

jest ustawiony. Nazwa parametru ze zdarzenia obiektu deweloperskiego jest oczekiwana jako parametr name.

Przykład
com.cisag.app.internal.obj.SupportRequest boRequest=….; CisParameterList parameterList=new CisParameterList(); parameterList.setObject(„newRequest“, boRequest);

Jeśli do zdarzenia mają zostać przekazane typy prymitywne, należy skorzystać z odpowiednich metod klasy CisParameterList.
Przykładowo, dla typu danych String używana jest metoda:

public void setString(String name, String value);

W obiekcie deweloperskim typu Zdarzenia takie typy proste są definiowane jako parametry typu logicznego (logischer Datentyp).

Zdarzenia mogą być wywoływane na różnych bazach danych: OLTP, OLAP, repozytorium oraz konfiguracyjnych. Jeśli podczas wywołania metody nie zostanie określona żadna baza danych, system domyślnie wykorzystuje bazę OLTP.

Podtypy zdarzeń

Niektóre metody fireEvent oczekują krótkiej wartości jako dodatkowego parametru. Służy ona do identyfikacji podtypu zdarzenia. Obiekty wywołujące zdarzenia mogą być typowane za pomocą ValueSets, którego wartość jest następnie przekazywana w celu identyfikacji podtypu metody. Podtypy są używane do ograniczenia liczby zdarzeń. Jeśli Na przykład, jeśli w aplikacji można wywołać kilka różnych zdarzeń, ale mają one te same parametry, można użyć jednego zdarzenia za pomocą podtypów.

Czy ten artykuł był pomocny?