1 Themenübersicht
Der Workflow in Semiramis basiert auf Aktivitäten und Aufgaben, die in der Semiramis-Workflow-Engine verarbeitet werden. Aktivitäten können sowohl auf den OLTP-Datenbanken als auch auf der Repository-Datenbank angelegt werden. Im ersten Teil dieses Dokuments wird beschrieben, wie Sie mithilfe der bereitgestellten Workflow-Schnittstellen Aktivitäten aus Ihren Anwendungen heraus erstellen und bearbeiten können. Anhand von Quellcodebeispielen zu den einzelnen Punkten wird erläutert, mit welchen Schritten Sie das gewünschte Ziel erreichen.
Im Workflow werden an vielen Stellen Terme, Bedingungen und Befehle verwendet, um komplexe Zusammenhänge auszudrücken. Alle diese Ausdrücke sind Teil einer gemeinsamen Skriptsprache. Die Syntax der Skriptsprache lehnt sich an SQL, Pascal und Java an. Über eine vorgegebene Programmierschnittstelle kann die Skriptsprache um neue Befehle und Funktionen erweitert werden. Insbesondere wenn komplexe Bedingungen geprüft werden sollen, erreichen Sie die Grenzen der Skriptsprache in Bezug auf die Ausdrucksfähigkeit oder Leistungsfähigkeit. In diesem Fall können Sie die Probleme über spezialisierte Funktionen beheben. Im zweiten Teil dieses Dokument wird erklärt, wie die Skriptsprache um neue Funktionen erweitert werden kann.
2 Zielgruppe
- Entwickler
3 Begriffsbestimmung
ActivityContainer
Der ActivityContainer ist ein wieder verwendbarer Container für Aktivitäten. Der Container stellt alle wesentlichen Methoden bereit, um Aktivitäten zu bearbeiten. Im Diagramm sind aus Gründen der Übersicht nur die Getter-Methoden eingetragen. Vom ActivityContainer aus kann auf die Workitems, SeriesElements, Attachments und Performer zugegriffen werden.
Klassendiagramm des ActivityContainers und seiner abhängigen Objekte.
Workitem
Ist eine Aktivität vom Typ STANDARD oder SERIES_ELEMENT, so kann diese Aktivität Aufgaben besitzen. Das Interface Workitem bietet eine nicht änderbare Sicht auf eine Aufgabe. Der Status, der Bearbeiter und die Zeitpunkte der Statuswechsel (nicht im Diagramm aufgeführt) können am Interface abgefragt werden.
SeriesElement
Wenn die Aktivität vom Typ SERIES ist, so kann sie zu festgelegten Zeitpunkten weitere Aktivitäten erzeugen. Das Interface SeriesElement stellt eine verkleinerte Sicht auf eine Aktivität dar. Die Daten dieses Interfaces können genutzt werden, um die Serienelemente in Listen oder Tabellen darzustellen. Mit den darin enthaltenen Schlüsseln können auch die entsprechenden Aktivitäten über den CisWorkflowManager und einen ActivityContainer nachgeladen werden. Die Attribute des SeriesElement können nicht geändert werden.
Attachment
An eine Aktivität können beliebig viele Verknüpfungen (Attachments) angebunden werden. Das Ziel einer Verknüpfung kann ein frei wählbares Business Entity sein. Einzige Bedingung ist, dass das Objekt auf der gleichen Datenbank wie die Aktivität persistent sein muss.
Performer
Der Performer legt fest, wer die Aufgaben einer Aktivität bekommt. Dabei kann es sich um eine einzelne Person oder eine Gruppe von Personen handeln. Jede Person erhält pro Aktivität eine eigene Aufgabe zugeordnet.
4 Vorgehen
4.1 CisWorkflowManager und ActivityContainer
Zentraler Einstiegspunkt für die Programmierung des Workflows bilden die beiden Klassen CisWorflowManager und ActivityContainer.
Der ActivityContainer stellt eine Sicht auf eine Aktivität und dessen abhängige Objekte dar. Der ActivityContainer ist wieder verwendbar, d. h. ein einmal angelegter ActivityContainer kann für mehrere Aktivitäten in Folge verwendet werden.
Während der ActivityContainer zur Bearbeitung eine Aktivität dient, ist der CisWorkflowManager für den Persistenzdienst verantwortlich. Mithilfe des CisWorkflowManagers lassen sich Aktivitäten anlegen, laden, speichern usw.
4.1.1 Methoden zum Setzen und Lesen der Datenbank-GUID und des Codes
Am ActivityContainer sind Methoden zum Setzen und Lesen der UserDatabaseGuid und des UserCodes sowie Methoden zum Lesen der DatabaseGuid und des Codes vorhanden.
Für alle Persistenzdienst-Operationen werden die Methoden mit „User“ benutzt. Um also eine Aktivität zu laden, müssen die Methoden setUserDatabaseGuid(…) und setUserCode(…) aufgerufen werden. Anschließend ist die Methode zum Laden der Aktivität am CisWorkflowContainer aufzurufen.
Die beiden Methoden getDatabaseGuid() und getCode() enthalten immer die entsprechenden Daten der aktuellen Aktivität des ActivityContainers, während die beiden Methoden getUserDatabaseGuid() und getUserCode() auch andere Werte annehmen können, z. B. um nachfolgend eine Aktivität zu laden.
4.1.2 Transaktionsverhalten
Der CisWorkflowManager ermöglicht es, einen ActivityContainer in eine bestehende Transaktion einzubinden oder diesen in einer eigenen Top-Level-Transaktion zu verwenden. Entschieden darüber wird zum Erstellungszeitpunkt des ActivityContainers. Ein nachträgliches Ändern des Verhaltens ist nicht möglich.
4.1.2.1 Einbinden in eine bestehende Transaktion
Soll der ActivityContainer in eine bestehende Transaktion eingebunden werden, so muss der ActivityContainer mithilfe der Methode createEmptyActivityContainer() am CisWorkflowManager erzeugt werden.
CisWorkflowManager wm = CisWorkflowManager.getInstance();
ActivityContainer ac = wm.createEmptyActivityContainer();
Wichtig beim Einbinden in bestehende Transaktionen ist, dass die umschließende Transaktion die gleiche Datenbank benutzt wie der ActivityContainer.
CisEnvironment env = CisEnvironment.getInstance();
CisTransactionManager tm = env.getTransactionManager();
byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);
ac.setUserDatabaseGuid(dbGuid);
Innerhalb einer eigenen Transaktion kann dann der ActivityContainer wie folgt aufgerufen werden.
byte[] transactionGuid = tm.beginNew(CisTransactionManager.OLTP);
try {
…
CisWorkflowManager wm = CisWorkflowManager.getInstance();
wm.update(ac);
…
tm.commit(transactionGuid);
} catch …
4.1.2.2 Benutzen einer eigenen Top-Level-Transaktion
Wenn der ActivityContainer nicht mit anderen Objekten in der gleichen Transaktion persistent gemacht werden muss, so kann eine eigene Top-Level-Transaktion benutzt werden. Alle Transaktionen werden dann innerhalb des ActivityContainers bzw. des CisWorkflowManagers durchgeführt. Um einen solchen ActivityContainer zu erhalten, ist die Methode createEmptyStandaloneActivityContainer() am CisWorkflowManager aufzurufen. Die Datenbank-GUID kann wie beim anderen ActivityContainer jeweils passend gesetzt werden.
CisWorkflowManager wm = CisWorkflowManager.getInstance();
ActivityContainer ac = wm.createEmptyStandaloneActivityContainer();
byte[] dbGuid = tm.getDatabaseGuid(CisTransactionManager.OLTP);
ac.setUserDatabaseGuid(dbGuid);
4.1.3 Erzeugen von Aktivitäten
Um eine Aktivität zu erzeugen, müssen Sie einen ActivityContainer erzeugen, die Datenbank festlegen und die entsprechende Methode am CisWorkflowManager aufrufen.
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);
4.1.4 Laden von Aktivitäten
Zum Laden einer Aktivität in einen ActivityContainer müssen die Datenbank-GUID und der Code der gewünschten Aktivität am Container in die Methoden setUserDatabaseGuid und setUserCode gesetzt werden. Anschließend ist am CisWorkflowManager die Methode load(…) aufzurufen.
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);
4.1.5 Speichern von Aktivitäten
Um eine Aktivität zu speichern, muss die Methode update(…) am CisWorkflowManager aufgerufen werden. Anschließend ist die Methode reload(..) aufzurufen, um den Container mit den aktuellen Daten zu versorgen. Es ist wichtig, diese Methode aufzurufen, da z. B. nach dem Speichern Workitems erstellt wurden, die am ActivityContainer erst durch die Methode reload(…) für Sie sichtbar werden.
Ist der Container in einer anderen Transaktion eingebunden, so darf reload(…) erst nach dem Commit aufgerufen werden.
CisWorkflowManager wm = CisWorkflowManager.getInstance();
ActivityContainer ac = …;
wm.update(ac);
wm.reload(ac);
4.1.6 Löschen von Aktivitäten
Aktivitäten können nicht gelöscht werden. Stattdessen kann der Status der Aktivität auf CANCELED oder DONE gesetzt werden.
4.1.7 Duplizieren von Aktivitäten
Beim Duplizieren einer Aktivität werden nur die Daten dupliziert, die für die Anlage der neuen Aktivität benötigt werden. Der Status, die Aufgaben etc. werden nicht kopiert. Die duplizierte Aktivität befindet sich im Erfolgsfall im übergebenen ActivityContainer.
CisWorkflowManager wm = CisWorkflowManager.getInstance();
ActivityContainer ac = …;
boolean successfull = wm.duplicate(ac);
4.1.8 Erstellen einer Kopie des ActivityContainers
Für den Fall, dass eine exakte Kopie eines ActivityContainers benötigt wird, existiert die Methode getCopy(). Die Kopie beinhaltet allerdings nicht die Texte, die mithilfe getText(…) abgefragt werden können.
4.1.9 Flags
Für die Oberflächenprogrammierung, insbesondere für die Darstellung des Anwendungsmodus, ist es wichtig zu wissen, in welchem Zustand sich der ActivityContainer befindet.
Die folgende Auflistung zeigt die verfügbaren Methoden am ActivityContainer.
- isPersistent()
- isValid()
- isChanged()
- isReadOnly()
- isStateChangeAllowed()
- isDatabaseGuidChanged()
- isCodeChanged()
4.2 Serien
Serien stellen eine Spezialform von Aktivitäten dar. Anders als Aktivitäten besitzen Serien keine Workitems. Stattdessen erzeugen Serien im Laufe ihrer Lebensdauer zu festgelegten Zeitpunkten andere Aktivitäten. Diese Aktivitäten können über die Methode getSeriesElements() abgefragt werden.
4.2.1 Anlegen einer Serie
Um eine Serie anzulegen, muss zuerst eine Aktivität, analog zu dem bekannten Muster, angelegt werden.
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);
…
Um aus einer „normalen“ Aktivität eine Serie zu machen, muss der Typ auf SERIES umgestellt werden.
Der Startzeitpunkt einer Serie stellt immer den nächsten Zeitpunkt dar, an dem eine Aktivität erzeugt wird. Dieser Zeitpunkt ändert sich nach jeder erstellten Aktivität. Die erzeugte Aktivität erhält den aktuellen Startzeitpunkt der Serie als Startzeitpunkt, und als Endzeitpunkt wird zum Startzeitpunkt die „seriesDuration“ hinzuaddiert.
Das Serienintervall und der zugehörige Kalender werden benutzt, um den nächsten Zeitpunkt zu berechnen, an dem eine Aktivität erzeugt werden soll.
…
ac.setType(ActivityType.SERIES);
ac.setStartTime(…);
ac.setEndTime(…);
ac.setSeriesDuration(…);
ac.setSeriesInterval(…);
ac.setSeriesIntervalCalendar(…);
…
wm.update(ac);
…
4.2.2 Serienintervall und Kalender
Ein Serienintervall ist ein symbolischer Zeitpunkt, wie z. B. „Am 2. Dienstag jedes Monats um 17:15 Uhr“ oder „Jeden Montag und Dienstag um 15 Uhr“. Diese Zeitpunkte sind immer abhängig vom Kalender, auf dem die Berechnung basieren soll. So ist „17:15 Uhr“ in Europa ein anderer Zeitpunkt als in Asien.
Zur Eingabe eines symbolischen Zeitpunkts gibt es für die Oberflächenprogrammierung das SymbolicIntervalField. Dieses enthält in einem Popup-Fenster alle möglichen Varianten für die symbolischen Zeitpunkte und besitzt einen einstellbaren Kalender. Das Feld besitzt die beiden Methoden getValue() und getCisCalendar(), um an die benötigten Werte für die Einstellung einer Serie zu gelangen.
SymbolicIntervalField field = new SymbolicIntervalField(…);
…
ac.setSeriesInterval(field.getValue());
ac.setSeriesIntervalCalendar(field.getCisCalendar());
…
4.3 Verknüpfungen (Attachments)
Zu jeder Aktivität können beliebig viele Verknüpfungen mit anderen CisObjects angelegt werden. Bedingung für die Verknüpfung ist, dass sich Aktivität und CisObjects auf der gleichen Datenbank befinden. Die Verknüpfungen werden in Form von Attachments mit der Aktivität gespeichert.
Attachment attachment = ac.createAttachment();
attachment.setCisObject(aCisObject);
attachment.setDescription(“…”);
ac.getAttachments().add(attachment);
Anhand der Reihenfolge in der Liste wird beim Speichern, beginnend bei Null und aufsteigend, die SequenceNumber vergeben. Dem ersten Attachment in der Liste, also mit der SequenceNumber Null, kommt eine besondere Bedeutung für das Starten der Anwendung zu einer Aufgabe zu.
4.4 Einstellen von Anwendungen für die Aufgaben
Die Aufgaben einer Aktivität werden mit den in Semiramis zur Verfügung stehenden Anwendungen bearbeitet. In einer Aktivität kann festgelegt werden, welche Anwendung geöffnet werden soll. Dies kann sowohl direkt über die Bestimmung einer Anwendung erfolgen als auch indirekt über die Verknüpfungen.
4.4.1 Anwendung direkt einstellen
Jede Anwendung in Semiramis kann über eine GUID eindeutig identifiziert werden. Diese GUID muss am ActivityContainer gesetzt werden. Des Weiteren kann festgelegt werden, welche ActionId und Parameter der Anwendung übergeben werden. Die möglichen ActionIds und Parameter sind im Entwicklungsobjekt Anwendung zur jeweiligen Anwendung festgelegt.
Im folgenden Beispiel wird die aktuelle Anwendung als Anwendung für eine Aktivität festgelegt.
CisApplicationManager am = env.getApplicationManager();
…
byte[] applicationGuid = am.getApplicationGuid();
CisParameterList actionParameters = new CisParameterList();
…
ac.setApplicationGuid(applicationGuid);
ac.setActionId((short) 1);
ac.setActionParameters(actionParameters);
4.4.2 Anwendung indirekt einstellen
Wird keine Anwendung direkt eingestellt, so werden die Verknüpfungen für die Ermittlung der Anwendung benutzt. Dabei wird die Verknüpfung herangezogen, welche als SequenceNumber die Null besitzt. Für das darin enthaltene Objekt wird die DefaultAnwendung ermittelt und gestartet. Das Objekt selbst ist dabei ein Übergabeparameter.
4.5 Aufgabenbearbeiter
Für eine Aktivität, mit Ausnahme der Serie, werden beim Wechsel in den Status WAITING Aufgaben erzeugt. Dabei wird für jeden Bearbeiter eine eigene Aufgabe erstellt. Die Ermittlung der Bearbeiter erfolgt je nach Einstellung im Performer.
Der Performer legt fest, wie die Bearbeiter einer Aufgabe ermittelt werden sollen. So können die Bearbeiter über die Partnerbeziehungen aus einer Stelle, einer Organisation oder einer Person kommen. Bei einem Partner ist hinterlegt, welcher Benutzer (User) zu diesem Partner gehört. Jedem ermittelten Benutzer wird eine Aufgabe zugewiesen.
Neben der Ermittlung über den Partner kann der Benutzer auch direkt angegeben werden. Alternativ, wenn gleich mehrere Benutzer eine Aufgaben erhalten sollen, lassen sich Benutzer auch in Workflowrollen zusammenfassen.
Werden für die Ermittlung der Benutzer Bindungen und Funktionen benötigt, so kann auch ein Ausdruck formuliert und im Performer eingetragen werden. Dadurch können z. B. solche Situationen abgebildet werden, die ab einem bestimmten Bestellwert einen Vorgesetzen als Bearbeiter der Aufgabe benötigen.
Das folgende Beispiel legt als Bearbeiter den aktuellen Benutzer fest.
Performer p = ac.getPerformer();
p.setType(PerformerType.USER);
p.setGuid(env.getUserGuid());
Immer wenn mehr als ein Benutzer existiert, also mehr als eine Aufgabe erstellt wurden, ist wichtig zu unterscheiden, ob alle oder nur eine der Aufgaben bearbeitet werden müssen, um die Aktivität zu erledigen. Mit der Methode setAllocation(…) kann das gewünschte Verhalten eingestellt werden.
Im folgenden Beispiel müssen alle Benutzer ihre Aufgaben bearbeiten, damit die Aktivität erledigt werden kann.
Performer p = ac.getPerformer();
p.setType(PerformerType.ROLE);
p.setAllocation(PerformerAllocation.ALL);
p.setGuid(env.getUserGuid());
4.5.1 Bearbeiter bei Zeitüberschreitung
Wenn der Bearbeitungszeitraum der Aktivität überschritten wird, kommt es je nach Einstellung zum Eskalationsverhalten. Ob dieser Fall eintritt, kann mithilfe der Methode setEscalationType im ActivityContainer festgelegt werden. Wird hier FORWARD eingestellt, so werden bei Zeitüberschreitung alle Aufgaben ihren Benutzern entzogen und neue Aufgaben für andere Benutzer erstellt.
Die „anderen“ Benutzer lassen sich am EscalationPerformer einstellen.
ac.setEscalationType(EscalationType.FORWARD);
Performer ep = ac.getEscalationPerformer();
ep.setType(PerformerType.USER);
ep.setAllocation(PerformerAllocation.ALL);
ep.setGuid(env.getUserGuid());
4.6 Bedingungen
In den Aktivitäten können Bedingungen festgelegt werden. Wird der Methode setCondition(…) eine Bedingung übergeben, so wird später in der Workflow Engine bei jedem Statuswechsel überprüft, ob die Bedingung erfüllt ist. Ist sie es nicht mehr, so wird die Aktivität, und folglich auch alle Aufgaben, auf CANCELED gesetzt.
Analog dazu kann eine Bedingung für eine Serie gesetzt werden. Hierfür steht die Methode setSeriesCondition(…) zur Verfügung. Ist diese Bedingung nicht mehr erfüllt, werden keine weiteren Aktivitäten erzeugt.
Eine Beschreibung der Syntax für Bedingungen findet sich im Dokument über die Workflow Engine.
4.7 Ändern des Aktivitätsstatus
Der Status einer Aktivität kann manuell auf DONE oder CANCELED geändert werden. Dieses wird z. B. dann benutzt, wenn eine Aktivität versehentlich angelegt wurde. Das Ändern des Status hat zur Folge, dass beim Speichern der Aktivität alle Aufgaben ebenfalls geändert werden.
ac.setState(ActivityState.DONE)
ac.setState(ActivityState.CANCELED)
4.8 Klassifikationen
Um die Menge gefundener Aktivitäten in Suchen und Abfrageanwendungen eingrenzen zu können, besitzt der ActivityContainer mehrere Methoden, um eine Aktivität zu klassifizieren. Der SourceType gibt die Herkunft der Aktivität an. Zumeist handelt es sich dabei um so genannte „Ad hoc“-Aktivitäten.
ac.setGroup1(…);
ac.setGroup2(…);
ac.setGroup3(…);
ac.setHierarchy1(…);
ac.setHierarchy2(…);
ac.setActivityClass(…);
ac.setSourceType(…);
4.9 Sonstiges
Confirmation
Mithilfe der Methode setConfirmation(…) wird festgelegt, ob die Bearbeitung einer Aufgabe explizit bestätigt werden muss. In Semiramis ist dazu in der Workflow-Symbolleiste eine Schaltfläche zu betätigen. Ist die Bestätigung nicht erforderlich, wechselt die Aufgabe bereits beim Öffnen in den Status „Erledigt“.
Workitems
Eine Liste der Aufgaben einer Aktivität erhält man mit der Methode getWorkitems(). Die darin enthaltenen Workitems sind nicht veränderbar.
WorkState
Wenn es in der Workflow Engine bei der Bearbeitung einer Aktivität zu Fehlern gekommen ist, wird die Aktivität gestoppt, d. h. von jeglicher weiteren Bearbeitung ausgenommen. Abfragen lässt sich der Arbeitsstatus einer Aktivität mithilfe getWorkState().
CrmType
Der CrmType wird ausschließlich für das Beziehungs-Management benötigt. Über die Angabe der GUID im CrmType können alle Aktivitäten ermittelt werden. die zu einem Beziehungs-Management-Element gehören.
DefinitionGuid
Basiert eine Aktivität auf einer Aktivitätsdefinition, so kann die GUID dieser Aktivitätsdefinition mithilfe der Methode getDefinitionGuid() abgefragt werden.
SeriesActivityGuid
Basiert die Aktivität auf einer Serie, so kann die GUID der Serie mithilfe der Methode getSeriesActivityGuid() erfragt werden.
Priority
Eine Aktivität kann Prioritäten zwischen 1 (hoch) und 9 (niedrig) annehmen.
4.9.1 Übersetzungen
Aktivitäten in Semiramis besitzen übersetzbare Elemente: der Bezeichnungstext und die Beschreibung.
Die Bezeichnung kann mithilfe getDescription() am ActivityContainer abgefragt werden, und das zugehörige NLS-Data mithilfe getDescriptionNLSData().
Neben einer Bezeichnung kann eine Beschreibung in mehreren Sprachen erfasst werden. Diese Beschreibung kann z. B. als detaillierte Aufgabenbeschreibung genutzt werden.
Sring text = ac.getText(“de”);
ac.setText(“de”, “text…”);
4.10 Klassen und Interfaces
Alle für die Erweiterung der Skriptsprache notwendigen Klassen und Interfaces befinden sich im Package com.cisag.pgm.util.
Folgende Klassen/Interfaces sind für die Erweiterung von Interesse:
- ParserLogic
- ParserEnvironment
- ParserExpression
- ParserFunction
- ParserResult
- ParserType
4.10.1 ParserLogic
Die Klasse ParserLogic stellt für Bedingungen, Ausdrücke und Befehlssequenzen je eine Methode bereit, um aus einer Zeichenkette einen Parsebaum zu erstellen. Der Baum besteht aus ParserExpressions. Als Ergebnis der Methoden wird das oberste Element dieses Baumes zurückgegeben.
Die Klasse ist zusätzlich auch ein Cache für die erstellen Parsebäume, sodass im Cache vorhandene Zeichenketten nicht erneut geparst werden müssen.
public ParserExpression getStatement(String statement)
throws ParserException {…}
public ParserExpression getCondition(String condition)
throws ParserException {…}
public ParserExpression getExpression(String expression)
throws ParserException {…}
4.10.2 ParserEnvironment
Die Klasse ParserEnvironment stellt die Laufzeitumgebung für ParserExpressions dar. Der Methode evaluate(…) kann eine ParserExpression übergeben werden. Diese wird ausgewertet, und das Ergebnis wird als ParserResult zurückgegeben.
public ParserResult evaluate(ParserExpression expression)
throws ParserException {…}
4.10.3 ParserExpression
Die Klasse ParserExpression stellt die abstrakte Oberklasse einer Funktion dar. Die Methode evaluate(…) enthält die eigentliche Funktion. Als Eingabeparameter wird ein Array mit ParserResults übergeben. Rückgabewert ist ein einzelnes ParserResult. Die Methode wird für alle möglichen Signaturen aufgerufen, d. h. innerhalb der Methode muss erkannt werden, welche Signatur für den Aufruf verwendet wurde.
ParserExpressions werden unter Umständen OLTP- und Session-übergreifend verwendet. Die Klasse ParserExpression darf daher keine Session- oder OLTP- spezifische Instanzenvariablen besitzen, d. h. insbesondere keine Manager aus com.cisag.pgm.appserver und keine Instanzen von Logikklassen.
4.10.4 ParserFunction
Die Klasse ParserFunction ist eine Fabrik und die abstrakte Oberklasse für alle Funktionsfabriken. Um eine oder mehrere eigene Funktionen zu erstellen, ist diese Klasse zu beerben. Zwei Methoden sind dabei zu implementieren: getNames() und getExpression(…).
Die erste Methode, getNames(), liefert die Namen aller Funktionen, die in dieser Fabrik erzeugt werden können. Die zweite Methode liefert zu einem Funktionsnamen die gewünschte Funktion (ParserExpression).
Zu beachten ist, dass die Methode getExpression(…) für jeden Funktionsnamen nur einmalig aufgerufen wird. Die erzeugte Funktion selbst stellt eine Methode bereit, um im Bedarfsfall weitere Exemplare der Funktion erzeugen zu können.
4.10.5 ParserResult
Die Klasse ParserResult kann sowohl Parameter als auch Ergebnis einer Funktion sein. Der Datentyp des Objekts in einem ParserResult wird durch den ParserType bestimmt.
Der Inhalt eines ParserResults kann nicht geändert werden, es stehen lediglich entsprechende Getter-Methoden zur Verfügung. Zum Erzeugen eines ParserResults ist die entsprechende statische Create-Methode am ParserResult aufzurufen, z. B. createBoolean(boolean value).
4.10.6 ParserType
Der ParserType stellt die Metadaten für ein ParserResult bereit. In der Klasse selbst sind schon einige ParserTypes, wie z. B. STRING, GUID, NUMBER vordefiniert. Es können aber auch eigene Typen, z. B. basierend auf einem CisObject, erstellt werden.
4.11 Gültigkeitsbereich von Funktionen
Um korrekt zu funktionieren benötigen Funktionen die geeignete Umgebung, d. h. bestimmte Randbedingungen müssen erfüllt sein. Unterschieden wird zum Einem die Datenbank und zum Anderen, ob eine Aktivität benötigt wird oder nicht. Soll die Funktion z. B. in der Übergangsbedingung (Transition) der Aktivitätsdefinition genutzt werden, so kann die Funktion nicht auf die Aktivität zugreifen, da diese erst zu einem späteren Zeitpunkt erstellt wird.
Die Festlegung des Gültigkeitsbereichs einer Funktion erfolgt im Konstruktor der Funktion. An der Klasse ParserFunctions sind die möglichen Gültigkeitsbereiche in Form von Konstanten definiert.
- WF_ACTIVITY_REPOSITORY
- WF_ACTIVITY_OLTP
- WF_TRANSITION_OLTP
- WF_TRANSITION_REPOSITORY
Ist die Ausführbarkeit einer Funktion nur von der Datenbank abhängig, so kann auch auf eine der beiden zusammengefassten Konstanten zurückgegriffen werden.
- WF_REPOSITORY
- WF_OLTP
Kann die Funktion unabhängig von Datenbank und Aktivität benutzt werden, kann auch die folgende Konstante genutzt werden:
- WF_ALL
4.12 Registrieren von Funktionen
Neu erstellte Funktionen müssen in der Klasse CisInitalizer im Package com.cisag.app.general.log mit der Methode initParserFunctions(…) registriert werden.
public static void initParserFunctions(
ParserFunctionProvider functionProvider) {
functionProvider.addFunction(new WorkflowFunctions());
functionProvider.addFunction(new BookFunctions());
}
5 Erweitern der Skriptsprache
Anhand eines Beispiels soll erläutert werden, wie die Skriptsprache um den Ausdruck „loadBook“ erweitert werden kann.
5.1 Erstellen der Factory
5.1.1 Anlegen der Klasse
Das Buisness Object „Book“ befindet sich im Package com.cisag.app.edu.obj. Die neue Funktion wird wie folgt definiert:
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 {
….
}
5.1.2 Definieren des Methodennamens und möglicher Signaturen
In der erzeugten Klasse werden zwei Konstanten definiert. Die Konstante NAMES enthält ein String-Array mit einer Auflistung der Funktionsnamen. In diesem Fall die Funktion „loadBook“.
In der Konstante SIGNATURE_LOAD werden mögliche Signaturen für die Funktion hinterlegt. Eine Signatur kann aus keinem, einem oder aber auch vielen Parametern bestehen. Jeder Parameter besitzt einen Namen und einen Typ. Die Funktion „loadBook“ soll entweder mithilfe des Parameters „guid“ vom Typ GUID oder „number“ vom Typ STRING aufgerufen werden können.
Die Namen der Konstanten NAMES und SIGNATURE_LOAD sind frei wählbar.
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),
};
5.1.3 Konstruktor erstellen
Im Konstruktor wird festgelegt, für welche Bereiche diese Funktion zugelassen ist. Die notwendigen Konstanten finden sich in der Klasse ParserFunctions.
Mit der Methode setFlag lässt sich ein Bereich ein- bzw. ausschalten. Die Funktion „loadBook“ ist nur für OLTP-Datenbanken zugelassen.
public BookFunctions() {
setFlag(ParserFunction.WF_OLTP, true);
}
5.1.4 Weitere Funktionen
Die Klasse BookFunctions enthält zwei weitere Methoden zum Implementieren: getNames() und getExpression(String)
Die Methode getNames() liefert die Namen aller Funktionen, die in der Klasse definiert sind. In diesem Beispiel enthält die Konstante NAMES nur den Wert „loadBook“.
public String[] getNames() {
return NAMES;
}
Die Methode getExpression(String) wird nur einmalig aufgerufen. Sie erzeugt für jeden übergebenen Funktionsnamen eine Instanz der Klasse, in der die Funktion implementiert ist. Die erzeugte Klasse wird als Prototyp genutzt, um später weitere Instanzen zu erzeugen.
public ParserExpression getExpression(String name) {
if (name.equals(NAMES[0])) {
return new LoadExpression(0, 0);
} else {
throw new RuntimeException(“unkown function”);
}
}
5.2 Erstellen der Funktion
Die Implementierung der Funktion „loadBook“ erfolgt in der Klasse LoadExpression, welche als Inner-Class im Quelltext eingebaut worden ist.
Die Klasse LoadExpression erbt von ParserExpression und muss deshalb zwei abstrakte Methoden, evaluate(…) und create(…) implementieren. Die Methode getShortName() wird zusätzlich überschrieben und gibt den Namen der Funktion zurück.
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() {
…
}
}
5.2.1 Konstruktor
Der Konstruktor wird mit zwei int-Werten für Zeile und Spalte aufgerufen. Die beiden Werte geben die Position innerhalb des zu parsenden Textes an und werden für Fehlermeldungen genutzt.
Innerhalb des Konstruktors werden die Signaturen (guid und number) und der Rückgabewert gesetzt. In diesem Fall soll der Rückgabewert eine Instanz der Klasse Book beinhalten.
public LoadExpression(int line, int col) {
super(line, col, TYPE_FUNCTION);
setSignatures(SIGNATURE_LOAD);
setResultType(ParserType.createCisObject(
CisObjectUtility.getClassGuid(Book.class)));
}
5.2.2 Die Methode evaluate(…)
Innerhalb dieser Methode wird die Funktion ausgeführt. Zuerst wird überprüft, ob als Parameter null übergeben wurde. Wenn dem so ist, so ist auch das Ergebnis der Funktion null bzw. ParserResult.NULL. Dieses Verhalten sollte in allen Funktionen vorhanden sein.
Anschließend wird versucht, den Schlüssel für das gewünschte Objekt zu erzeugen. Hier werden die möglichen Signaturen ausgewertet. Für den Fall, dass der Parameter vom Typ ParserResult.STRING ist, wird der Schlüssel über die Methode buildByNumberKey(…) aufgebaut, für den Fall ParserResult.GUID wird die Methode buildPrimaryKey(…) genutzt. Kann die Signatur nicht ausgewertet oder der Schlüssel nicht ermittelt werden, sind geeignete Fehlermeldungen auszugeben.
Nachdem der Schlüssel ermittelt wurde, wird das Objekt nach bekanntem Schema geladen und als ParserResult verpackt.
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);
}
}
5.2.3 Die Methode create(…)
Die Methode create(…) dient zum Erzeugen einer neuen ParserExpression-Instanz. Nachdem die Methode getExpression(…) einen Prototypen der ParserExpression erstellt hat, werden alle weiteren ParserExpressions durch den Aufruf dieser Methode am Prototypen erstellt.
Wichtig ist, dass das Erzeugen der neuen Klasse über Classreflection erfolgt, damit eventuelle Ersetzungen berücksichtigt werden können. Bei Ersetzungen ist zu berücksichtigen, dass diese die gleichen Parameter im Konstruktor tragen.
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);
}
}
5.2.4 Die Methode getShortName()
Die Methode gibt den Namen der Funktion zurück, in diesem Fall „loadBook“.
public String getShortName() {
return NAMES[0];
}
6 Ereignisse
In Semiramis werden zwei Arten von Ereignissen unterschieden, Business-Entity-Ereignisse und programmierte Ereignisse. Das Auslösen eines programmierten Ereignisses ist in der Logik eines Anwendungsprogramms explizit kodiert. Business-Entity-Ereignisse werden implizit bei der Änderung eines Business Objects durch den Persistenzdienst ausgelöst.
Beim Erstellen von Aktivitätsdefinitionen wird definiert, bei welchen Ereignissen sie Aktivitäten erzeugen sollen. Wenn ein Ereignis ausgelöst wurde, prüft die Workflow Engine, ob eine Aktivitätsdefinition auf das Ereignis reagieren soll, und benachrichtigt die entsprechenden Aktivitätsdefinitionen.
6.1 Auslösen der Ereignisse
Die programmierten Ereignisse werden in der Anwendung Entwicklungsobjekte als Entwicklungsobjekt-Ereignis definiert. Hier wird definiert, welche Parameter das Ereignis bekommen soll, auf welcher Datenbank das Ereignis stattfindet und ob es Subtypen des Ereignisses geben soll.
Die Anwendungsentwickler definieren in den Anwendungen, wann die Ereignisse ausgelöst werden. Das Interface com.cisag.pgm.appserver.CisSystemManager bietet hierfür die Methode fireEvent mit unterschiedlichen Parametern an. Über diese Methode wird ein Ereignis ausgelöst, welches die Workflow Engine verarbeitet.
Als Minimum an Parametern erwartet die Methode den Namen des Entwicklungsobjektes Ereignis und die Parameterliste:
fireEvent(String name, CisParameterList parameters).
Der Name ist der vollqualifizierte Name des Entwicklungsobjektes Ereignis, z.B. „com.cisag.app.internal.log.SupportRequestNotification“.
Der Übergabeparameter parameters erwartet die in dem Entwicklungsobjekt Ereignis definierten Parameter. Die Parameterliste muss sämtliche in dem Entwicklungsobjekt definierten Parameter enthalten. Sind in dem Entwicklungsobjekt Ereignis mehr Parameter definiert, als in der der Methode übergebenen Parameterliste vorhanden sind, so wird eine Exception ausgelöst. Im Entwicklungsobjekt Ereignis können zwei Typen von Parametern definiert werden, Parameter vom Typ Business Object und vom Typ logischer Datentyp. Die Parameter vom Typ Business Object werden an der CisParameterList mit der Methode
setObject(String name, Object value)
gesetzt. Als Parameter name wird der Parametername aus dem Entwicklungsobjekt Ereignis erwartet.
Beispiel:
com.cisag.app.internal.obj.SupportRequest boRequest=….;
CisParameterList parameterList=new CisParameterList();
parameterList.setObject(„newRequest“, boRequest);
Wenn primitive Datentypen übergeben werden sollen, so werden die entsprechenden Methoden der CisParameterList aufgerufen, z. B. für den Datentyp String:
public void setString(String name, String value).
Im Entwicklungsobjekt Ereignis werden die primitiven Datentypen über Parameter vom Typ logischer Datentyp definiert.
Ereignisse können auf OLTP-, OLAP-, Repository- und Konfigurationsdatenbanken ausgelöst werden. Wird beim Aufrufen der Methode keine Datenbank angegeben, wird als Standard die OLTP-Datenbank verwendet.
6.2 Subtypen von Ereignissen
Einige der fireEvent-Methoden erwarten als zusätzlichen Parameter einen Short-Wert. Dieser dient zur Identifizierung des Subtyps eines Ereignisses. Die Entwicklungsobjekte Ereignis können mit Valuesets typisiert werden. Ein Wert des Valuesets wird dann zur Identifikation des Subtyps der Methode übergeben. Die Subtypen dienen dazu, die Anzahl der Ereignisse zu begrenzen. Wenn z. B. in einer Anwendung mehrere verschiedene Ereignisse ausgelöst werden können, diese aber dieselben Parameter besitzen, so kann mit Hilfe der Subtypen ein Ereignis verwendet werden.