Technische Referenz
Aus Salespoint
Inhaltsverzeichnis
|
Einleitung
Zweck der Technischen Referenz
In der technischen Referenz wird der interne Aufbau von SalesPoint sehr detailliert beschrieben. Sie richtet sich an künftige Entwickler des Frameworks, aber auch an Studenten, die wissen wollen, was jenseits der Schnittstellen abläuft. Gute Java- und SalesPointkenntnisse werden dabei vorausgesetzt. Vermutlich wird das Dokument erst ab der Hälfte der Implementierungsphase interessant, wenn man mit den beschriebenen Datenstrukturen bereits Programmiererfahrungen gesammelt und einige der Probleme kennengelernt hat.
Die Komplexität von SalesPoint macht es sehr mühsam, durch reines Quellcodestudium zu verstehen, was passiert. Die technische Referenz orientiert sich sehr stark am Quellcode ausgewählter Klassen und versucht im Hinblick auf mögliche Seiteneffekte zu erläutern, warum diese oder jene Entwurfsentscheidung getroffen wurde. Der Quellcode von SalesPoint ist über die Javadoc einsehbar, indem in der Detailansicht auf einen Klassen- oder Methodennamen geklickt wird.
Ein Hinweis an die Leser
Das Framework ist sehr umfangreich und noch längst ist nicht alles in Form der Technischen Referenz dokumentiert. Es wird, hoffentlich in kurzen Abständen, immer mal wieder ein neues Kapitel veröffentlicht werden. Der Autor freut sich über konstruktive Hinweise zu diesem Dokument (Ist etwas unverständlich? Ist etwas schlecht formuliert? Sind da Tippfehler?). Aber auch Wünsche für das nächste Thema, welches behandelt werden soll, werden berücksichtigt.
DataBaskets
Aufbau von DataBaskets
DataBaskets sind Container für Informationen, welche Transaktionen beschreiben. Dazu gehören Entfernen, Hinzufügen oder Editieren von CatalogItems oder StockItems. Die Informationen selbst werden durch Objekte vom Typ DataBasketEntry repräsentiert. DataBasketEntries werden allerdings nicht direkt im DataBasket gespeichert. Stattdessen enthält der DataBasket einen oder mehrere SubDataBaskets, welche ihrerseits die DataBasketEntries beherbergen.
Dadurch, dass mehrere SubDataBaskets gespeichert werden können, erhält man die Möglichkeit, mit einem DataBasket mehrere "Speicherpunkte" zu unterhalten. Um das dadurch eingeführte Problem der möglichen Datenkorruption durch Inkonsistenzen muss sich der Programmierer kümmern, das Framework führt keine Konsistenzprüfungen durch.
In der Praxis wird meist ein DataBasket mit genau einem SubDataBasket verwendet. Das ist auch die Standardkonfiguration beim Erzeugen eines neuen DataBaskets.
Aufbau von DataBasketEntries
DataBasketEntries beschreiben Transaktionen von Stock- oder CatalogItems und besitzen dafür die folgenden Attribute:
mainKey | Unterscheidung des Typs von DataBasketEntry: für StockItems oder für CatalogItems. |
secondaryKey | Der Key des Catalog- oder StockItems, für welches der DataBasketEntry eine Transaktion beschreibt. |
value | Das Catalog- oder StockItem, für welches der DataBasketEntry eine Transaktion beschreibt. Der Key dieses Items entspricht genau dem secondaryKey des DataBasketEntrys. |
source | Der Container (Catalog oder Stock), aus dem das Item entfernt wurde. |
destination | Der Container (Catalog oder Stock), zu dem das Item hinzugefügt wurde. |
owner | Der DataBasket, zu dem dieser DataBasketEntry gehört. |
handled | Beschreibt, ob schon ein commit oder rollback auf diesem DataBasketEntry durchgeführt wurde. |
DataBasketConditions
DataBasketConditions sind Filter für DataBasketEntries. Deshalb besitzen sie auch die meisten der Attribute von DataBasketEntries:
mainKey | Unterscheidung des Typs von DataBasketEntry: für StockItems oder für CatalogItems. |
secondaryKey | Der Key des Catalog- oder StockItems, für welches der DataBasketEntry eine Transaktion beschreibt. |
value | Das Catalog- oder StockItem, für welches der DataBasketEntry eine Transaktion beschreibt. |
source | Der Container, aus dem das Item entfernt wurde. |
destination | Der Container, zu dem das Item hinzugefügt wurde. |
Außerdem gibt es eine match() Methode, die standardmäßig true zurückliefert, in speziellen Fällen aber überschrieben werden muss (siehe checkItems(boolean fGet) in #Aufbau von SubDataBaskets).
Falls ein Attribut den Wert null besitzt, so wird dies vom Framework dahingehend interpretiert, dass der Filter bei diesem Attribut immer passt. Es gibt sozusagen kein Filterkriterium, welches angewandt werden müsste.
DataBasketConditions kommen unter Anderem beim Iterieren über SubDataBaskets zur Anwendung (siehe Iterieren über SubDataBaskets in #Aufbau von SubDataBaskets).
SubDataBaskets
Das Herzstück eines SubDataBaskets ist eine HashMap dbeCatagories. In ihr befinden sich wiederum zwei HashMaps, eine für StockItemDBEntries und eine für CatalogItemDataBasketEntries. Jede dieser zwei Maps enthält Listen. In diesen Listen stehen die eigentlichen DataBasketEntry-Objekte. Der Key, unter dem die Listen in ihrer Map gespeichert sind, ist gleich dem Key des Items, welches in Form eines DataBasketEntrys gespeichert werden soll (also dem secondaryKey des DataBasketEntrys). Damit ist es möglich, Items gleichen Typs und Namens, z.B. zwei CatalogItems, die beide den Namen "item1" haben, im selben SubDataBasket zu speichern, sprich, Kollisionen zu erlauben. folgende Abbildung verdeutlicht den Aufbau noch einmal grafisch.
An dieser Stelle bietet sich ein Beispiel an: Wenn drei mal je fünf StockItems "item1" aus einem CountingStock entfernt werden, gibt es drei DataBasketEntries, die in einer gemeinsamen Liste in der Map für die StockItemDBEntries stehen. Auf diese Liste kann vom Framework über den Key "item1" zugegriffen werden. Die drei DataBasketEntries sehen hier identisch aus, und zwar folgendermaßen:
mainKey | __MAINKEY:_STOCKITEM_IMPL |
secondaryKey | item1 |
value | 5 (bei Items für StoringStocks stände hier das tatsächliche StockItem Objekt) |
source | das Stock Objekt, aus dem die Items entfernt wurden |
destination | null |
Iterieren über SubDataBaskets
SubDataBaskets haben ihren eigenen Iterator, auf den mit der Methode iterator(DataBasketCondition dbc, boolean fAllowRemove, boolean fShowHandled) zugegriffen werden kann.
Der verschachtelte Aufbau von SubDataBaskets macht es nötig, in mehreren Stufen zu iterieren:
- über die HashMap dbeCategories. Der verwendete Iterator heißt iCategories.
- über die HashMaps, welche die Listen mit StockItemDBEntries und CatalogItemDataBasketEntries enthalten. Der verwendete Iterator heißt iSubCategories.
- über die Listen in den SubCategories, welche ihrerseits die eigentlichen DataBasketEntries enthalten. Der verwendete Iterator heißt iItems.
Alle drei Iteratoren sind global in der Klasse SubDataBasket definiert.
hasNext() und next()
Die Methoden hasNext() und next() machen beide Gebrauch von findNext(). Während hasNext() findNext() mit dem Parameter fGet = false aufruft und den erhaltenen booleschen Rückgabewert selbst zurückliefert, ist fGet für next() true. Der Rückgabewert ist die globale Variable dbeCurrent, in welcher der nächste zurückzuliefernde DataBasketEntry abgelegt wurde. Das Speichern des Entrys in dbeCurrent geschieht in der Methode checkItems(boolean fGet).
findNext(boolean fGet)
Die Methode besteht aus einer do-while-Schleife. Zu Beginn wird checkSubCategories(boolean fGet) aufgerufen. Wird dabei true zurückgeliefert, so wurde in der aktuellen Category (entweder die Map für StockItems oder die für CatalogItems) eine SubCategory mit passendem DataBasketEntry gefunden und, falls fGet true ist, wurde dieser Entry in dbeCurrent gespeichert. Es wird true zurückgeliefert und die Methode ist beendet.
Konnte kein DataBasketEntry gefunden werden, muss die nächste Category, falls es noch eine gibt, durchsucht werden. Diese erhält man, indem man den Iterator iCategories weiterlaufen lässt. Auf der neuen Category wird ein Iterator erzeugt und als iSubCategories gespeichert, er wird dann beim nächsten Durchlauf der do-while-Schleife von checkSubCategories() verwendet, um über die SubCategories dieser Category zu iterieren. Falls dem Iterator des DataBaskets im Konstruktor eine DataBasketCondition übergeben wurde, wird vor dem Weiterspringen zur nächsten Category überprüft, ob der mainKey der Condition null ist, bzw. ob die mainKeys von DataBasketEntry und DataBasketCondition identisch sind. Falls nicht, würde diese Category ausgelassen und im folgenden Durchlauf der do-while-Schleife die nächste untersucht. Gibt es keine Category mehr, wird findNext() mit return false verlassen.
checkSubCategories(boolean fGet)
Die Methode arbeitet absolut analog zu findNext(), nur eine Hierarchieebene tiefer. So wird zu Beginn der do-while-Schleife mit checkItems(boolean fGet) überprüft, ob passende DataBasketEntries in den Items der SubCategory zu finden sind. Falls ja, wird die Methode mit return true beendet, andernfalls wird zur nächsten SubCategory gegangen, auf der der Iterator mItems definiert wird, um über die Items der SubCategory zu laufen. Auch hier muss wieder die DataBasketCondition beachtet werden. Eine SubCategory kommt in Frage, wenn der secondaryKey der Condition mit dem Key der SubCategory übereinstimmt oder null ist.
checkItems(boolean fGet)
Die Methode überprüft zuerst dbeNext. Wenn es nicht null ist, wurde in einem früheren Iterationsschritt (ausgelöst von hasNext()) schon das Vorhandensein eines passenden DataBasketEntrys festgestellt und es wird true zurückgeliefert. Wenn zusätzlich fGet true ist, also checkItems() von der next() Methode aufgerufen wurde, wird vorher dbeNext in dbeCurrent gespeichert. Wenn dbeNext aber null ist, ist nicht bekannt, ob es einen weiteren DataBasketEntry gibt. Deshalb wird unter Benutzung von iItems über die aktuelle Liste von Items iteriert. Für jeden DataBasketEntry wird überprüft, ob er den Suchkritieren entspricht. Das ist der Fall wenn:
- er "unbearbeitet" ist (siehe #Commit von DataBasketEntries) oder er "bearbeitet" ist und zugleich fShowHandled (Parameter der iterator() Methode) true ist
- die DataBasketCondition null ist oder auf den DataBasketEntry passt, also in source, destination und value mit ihm übereinstimmt.
Wenn value der DataBasketCondition null ist, entscheidet stattdessen die match() Methode der DataBasketCondition, ob die Suchkriterien erfüllt sind. Damit lassen sich komplexere Filterungen durchführen, als es ein bloßes Setzen der Filterattribute ermöglicht. Beispielsweise kann überprüft werden, ob ein Attribut des DataBasketEntrys null ist (zur Erinnerung: null in einem Attribut der DataBasketCondition bedeutet, alles kann im zugehörigen Attribut des DataBasketEntrys stehen, eine explizite Abfrage nach null ist damit nicht möglich). Anwendung findet die match() Methode beispielsweise beim Hinzufügen von CatalogItems zu einem Catalog (siehe #Hinzufügen eines CatalogItems). Dort wird eine DataBasketCondition erzeugt, die unter Anderem überprüft, ob für einen DataBasketEntry das Attribut destination gleich null ist und das Attribut source ungleich null. Wird ein passender DataBasketEntry gefunden, so wird er in dbeNext gespeichert, oder, wenn fGet true ist, gleich in dbeCurrent. Anschließend wird die Methode mit return true beendet. Wurde in der Liste kein passender Eintrag gefunden, wird false zurückgeliefert.
Commit von DataBasketEntries
Ein commit() wird vom DataBasket an all seine SubDataBaskets weitergeleitet. Es kann eine DataBasketCondition übergeben werden. Das commit() wird dann nur für DataBasketEntries, die auf die DataBasketCondition passen, ausgeführt. Alternativ kann man mit commitSubBasket() auch einen bestimmten SubDataBasket angeben, für den ein commit durchgeführt werden soll. Es ist allerdings nicht möglich, einzelne SubDataBaskets für ein commit auszuwählen und ihnen gleichzeitig eine DataBasketCondition als Filter mitzugeben. Diese Funktionalität kann aber bei Bedarf leicht in einer Unterklasse von DataBasketImpl realisiert werden.
SubDataBaskets leiten das commit zu ihren Entries. Es wird mit der übergebenen DataBasketCondition über den SubDataBasket iteriert (siehe Iterieren über SubDataBaskets in #SubDataBaskets). Auf jeden DataBasketEntry, der vom Iterator geliefert wird, wird ein commit() ausgeführt, sofern der DataBasketEntry noch nicht als handled (bearbeitet) markiert ist. Anschließend wird er mit der remove() Methode des Iterators aus dem SubDataBasket entfernt.
Beim commit eines DataBasketEntries wird handled auf true gesetzt, um anzuzeigen, dass dieser Entry bearbeitet wurde und keine aktuelle Transaktion mehr beschreibt. Danach wird für die Quelle des DataBasketEntries, die im Feld source steht, ein commitRemove() ausgeführt, für das Ziel des Entrys, welches vom Attribut destination beschrieben wird, ein commitAdd(). Damit werden temporär hinzugefügte oder entfernte Items in ihren Quellen fest hinzugefügt bzw. komplett entfernt. Für Details zu diesen Methoden sei auf die Dokumentation von Katalogen und Stocks verwiesen.
Rollback von DataBasketEntries
Ein rollback() funktioniert analog zum commit(). Beim rollback() auf dem DataBasketEntry wird ein rollbackAdd() bzw. ein rollbackRemove() ausgeführt, was zur Folge hat, dass temporär zu einer Quelle hinzugefügte Items wieder entfernt werden und temporär entfernte Items wieder hinzugefügt. Auch zu diesen Methoden stehen Details in der Dokumentation von Catalogs und Stocks.
Die Bedeutung des Flags "handled"
Das Flag handled scheint auf den ersten Blick überflüssig zu sein, schließlich wird der abgearbeitete DataBasketEntry sofort aus dem SubDataBasket entfernt, wenn auf diesem ein commit() oder rollback() ausgeführt wird. Allerdings ist es möglich, für einen einzelnen DataBasketEntry "von Hand" ein commit() oder rollback() auszuführen und ihn einfach nicht aus dem SubDataBasket zu löschen. Um in solchen Fällen Datenintegrität zu sichern, werden bearbeitete DataBasketEntries immer mit handled entwertet.
Catalogs
Aufbau
Catalogs sind Container für CatalogItems. Die CatalogItems können innerhalb des Catalogs verschiedene Zustände haben.
- Sie können dem Catalog angehören.
- Sie können dem Catalog temporär angehören, das ist der Fall, wenn sie ihm mit einem DataBasket hinzugefügt werden.
- Sie können temporär aus dem Catalog entfernt sein, mit der Hilfe eines DataBaskets.
- Sie können den Status "editierbar" haben.
Ein Catalog kennt alle seine CatalogItems, zusätzlich steht in jedem CatalogItem, zu welchem Catalog es gehört. Im Folgen wird dafür die Bezeichung "Elternkatalog" verwendet.
Die vier oben beschriebenen Zustände werden intern durch 4 HashMaps repräsentiert:
mItems | enthält die CatalogItems dieses Catalogs. für persistente Kataloge ist dies eine PersistentMap. |
mTemporaryRemoved | enthält CatalogItems, die mit Hilfe eines DataBaskets entfernt wurden, bis ein commit/rollback ausgeführt wird. |
mTemporaryAdded | enthält CatalogItems, die mit Hilfe eines DataBaskets hinzugefügt wurden, bis ein commit/rollback ausgeführt wird. |
mEditingItems | enthält CatalogItems, die momentan editierbar sind. |
Editierbarkeit eines Kataloges
Da Kataloge selbst CatalogItems sind, ist es möglich, sie hierarchisch zu schachteln (Beachten sie das persistente Kataloge keine Kataloge aufnehmen können). Der Katalog, in dem ein anderer enthalten ist, ist für diesen der Elternkatalog. Es gelten für die Editierbarkeit von Katalogen die selben Regeln wie für die Editierbarkeit von CatalogItems, ein Katalog ist also editierbar wenn:
- er keinen Elternkatalog hat oder
- sich in mEditingItems des Elternkatalogs befindet, also explizit editierbar gemacht wurde.
Für Details siehe #Holen eines CatalogItems und #Geschachtelte Kataloge.
Hinzufügen eines CatalogItems
CatalogItems werden einem Catalog mittels der add(CatalogItem ci, DataBasket db) Methode hinzugefügt. Zuerst wird überprüft, ob der Catalog überhaupt editierbar ist, falls nicht, wird mit einer NotEditableException abgebrochen.
Dann wird nachgeschaut, ob das CatalogItem in einer der Maps steht, also schon im Katalog vorhanden ist. Falls das CatalogItem schon in mItems oder mTemporaryAdded steht, wird eine DuplicateKeyException geworfen und die Operation abgebrochen. CatalogItems dürfen nicht zweimal im selben Catalog vorkommen.
Befindet sich das CatalogItem in mTemporaryRemoved, hätte ein Hinzufügen den Effekt, dass das temporäre Entfernen rückgängig gemacht wird. Voraussetzung ist, dass Entfernen und Hinzufügen den selben DataBasket benutzen. Ist dies der Fall, wird ein rollback() auf das temporäre Entfernen ausgeführt und der Modifikationszähler erhöht, andernfalls wird eine DataBasketConflictException geworfen. Falls das temporär entfernte CatalogItem aber zwischenzeitlich einem anderen Katalog hinzugefügt wurde, resultiert das ebenfalls in einer DataBasketConflictException.
Nachdem diese Tests abeschlossen sind, findet das eigentliche Hinzufügen statt. Wurde null als DataBasket verwendet, wird das CatalogItem direkt in mItems gespeichert, andernfalls in mTemporaryAdded. Im zweiten Fall muss zusätzlich ein Eintrag im DataBasket angelegt werden, der das Hinzufügen beschreibt. Es wird für das CatalogItem ein CatalogItemDataBasketEntry erzeugt, der den aktuellen Katalog als Ziel hat. Es ist allerdings auch möglich, dass für das CatalogItem schon ein DataBasketEntry existiert. Das ist der Fall, wenn es temporär aus einem anderen Katalog entfernt wurde. In dem Fall wird der Entry entsprechend aktualisiert, so dass das temporäre Hinzufügen auch verzeichnet ist.
Zum Schluss wird der Modifikationszähler erhöht, das CatalogItem erhält eine Referenz auf den Catalog, zu dem es jetzt gehört, und Listener werden benachrichtigt, dass ein CatalogItem hinzugefügt wurde.
Entfernen eines CatalogItems
CatalogItems werden aus einem Catalog mittels der Methoden remove(CatalogItem ci, DataBasket db) oder remove(String sKey, DataBasket db) entfernt. Das entfernte CatalogItem wird dabei zurückgeliefert.
Die Methode remove(String sKey, DataBasket db) führt im Wesentlichen einige Tests durch und ruft remove(CatalogItem ci, DataBasket db) auf. Darum soll letztere genauer betrachtet werden.
Vor dem Entfernen wird überprüft, ob der Catalog editierbar ist, wenn nicht, wird eine NotEditableException geworfen und das Entfernen abgebrochen. Anschließend wird festgestellt, ob und in welcher Form das CatalogItem im Katalog vorhanden ist.
Befindet es sich in mTemporaryAdded, hat ein Entfernen unter Umständen den selben Effekt wie ein Rückgängigmachen des temporären Hinzufügens. Voraussetzung ist, dass beim Entfernen der selbe DataBasket wie für das Hinzufügen verwendet wird. Ist das der Fall, werden Listener befragt, ob das CatalogItem entfernt werden darf. Kommt kein Veto, so wird ein rollback() des temporären Hinzufügens ausgeführt. Falls im DataBasket aber steht, dass das CatalogItem nicht nur zu diesem Katalog hinzugefügt, sondern auch temporär aus einem anderen Katalog entfernt wurde, so darf kein richtiges rollback() stattfinden. Der Grund ist, dass bei einem rollback() auch das temporäre Entfernen aus dem anderen Katalog rückgängig gemacht würde. Stattdessen wird das rollback() des temporären Hinzufügens "von Hand" nachgebildet: Das destination Feld des DataBasketEntrys wird auf null gesetzt, das CatalogItem wird aus mTemporaryAdded entfernt, der Modifikationszähler erhöht und Listener über die Änderungen informiert. Zuletzt wird die Referenz des CatalogItems auf seinen Catalog gelöscht und das CatalogItem wird zurückgeliefert.
Falls das CatalogItem in mTemporaryRemoved steht, kann es nicht mehr entfernt werden. Deshalb wird eine DataBasketConflictException geworfen.
Als nächstes wird getestet, ob sich das CatalogItem in mItems befindet. Falls das nicht zutrifft, ist das CatalogItem nicht im Katalog enthalten und kann deshalb auch nicht entfernt werden. Die remove() Methode wird mit return null verlassen. Wurde das CatalogItem gefunden, wird es nun entfernt.
Zuerst werden Listener befragt, ob das CatalogItem überhaupt entfernt werden darf. Falls kein Listener Einwände hat, wird das CatalogItem aus mItems gelöscht. Ist der benutzte DataBasket nicht null, so ist das Item nur temporär zu entfernen. Deshalb wird es in mTemporaryRemoved gespeichert. Falls im DataBasket ein Eintrag existiert, nach dem das CatalogItem bereits einem anderen Katalog temporär hinzugefügt wurde, so wird im source Feld dieses Eintrags der aktuelle Katalog eingetragen. Das beschreibt eine Verschiebeoperation, wie sie zum Beispiel mit TwoTableFormSheets vorgenommen werden kann. Ansonsten wird ein neuer DataBasketEntry für das Entfernen aus dem Katalog im DataBasket gespeichert.
Zuletzt wird der Modifikationszähler erhöht, aus dem Item wird die Referenz auf den Katalog entfernt, Listener werden über das Entfernen des Items aus dem Katalog informiert und das CatalogItem wird zurückgeliefert.
Seiteneffekte und Veto beim Entfernen eines CatalogItems
Beim Entfernen eines CatalogItems wird Listenern die Möglichkeit gegeben, Einspruch einzulegen. Damit soll Datenkorruption verhindert werden. Nur ausgewählte Listener machen von ihrem Einspruchsrecht Gebrauch, nämlich die zu CountingStockImpl und StoringStockImpl gehörenden.
Die Stocks selbst implementieren nicht das Interface CatalogChangeListener, welches auf die Katalogänderungen hört, sondern überlassen das einer anonymen inneren Klasse. Diese wird jeweils in der Methode private void initReferentialIntegrityListener() definiert.
Werden CatalogItems entfernt, müssen auch die entsprechenden StockItems vernichtet werden. Allerdings gibt es Situationen, in denen das nicht möglich ist. In diesen Situationen, die nachfolgend erläutert werden, werden VetoExceptions geworfen, um das Entfernen des CatalogItems zu verhindern. CatalogItems dürfen nicht entfernt werden wenn:
- StockItems, deren CatalogItem entfernt werden soll, dem Stock nur temporär hinzugefügt sind. Das CatalogItem darf nur entfernt werden, wenn sich die StockItems fest im Stock befinden.
- StockItems, deren CatalogItem entfernt werden soll, temporär mit einem anderen DataBasket entfernt wurden. Dazu zählt auch, dass der aktuelle DataBasket null ist.Es wird gezählt, wieviele Einträge im DataBasket der remove() Operation das temporäre Entfernen der zugehörigen StockItems beschreiben. Diese Zahl wird mit der Anzahl der betroffenen StockItems in mTemporaryRemoved des Stocks verglichen. Ist die Anzahl in mTemporaryRemoved größer, so bedeutet das, dass es betroffene StockItems gibt, welche temporär mit einem anderen DataBasket entfernt wurden.
Dies gilt sowohl für CountingStocks als auch für StoringStocks. Aufgrund der unterschiedlichen Struktur der Stocks ist lediglich die Implementierung etwas abweichend voneinander.
Holen eines CatalogItems
CatalogItems können mit get(String sKey, DataBasket db, boolean fForEdit) aus einem Catalog geholt werden. Beim Holen eines CatalogItems wird mit dem Parameter fForEdit unterschieden, ob es editierbar sein soll oder nicht. Das CatalogItem kann zwar auch editiert werden, wenn es als nicht editierbar geholt wurde, allerdings bietet der DataBasket nur für editierbare Items die Möglichkeit, Änderungen mit einem rollback() rückgängig zu machen.
Wie beim Hinzufügen und Entfernen von CatalogItems wird auch hier zunächst eine Reihe von Tests durchgeführt. Zuerst wird überprüft, ob der Katalog selbst editierbar ist. Ist er das nicht, das zurückzuliefernde CatalogItem soll aber editierbar sein (fForEdit ist true), so wird eine NotEditableException geworfen.
Befindet sich das CatalogItem in mTemporaryAdded und es gibt einen zugehörigen Eintrag im DataBasket (das heißt, der aktuell verwendete DataBasket ist der selbe, mit dem das CatalogItem temporär hinzugefügt wurde), so wird es im Fall, dass es nicht editierbar sein soll, einfach zurückgeliefert. Soll es dagegen editierbar sein, wird vorher noch getestet, ob das temporäre Hinzufügen Teil einer Verschiebeoperation war. Denn dann gibt es einen Katalog, aus welchem das CatalogItem temporär entfernt wurde. Änderungen am CatalogItem müssten auch in diesen Katalog übernommen werden und umgekehrt. Dieser Fall wird vom Framework nicht unterstützt und stattdessen eine DataBasketConflictException geworfen. Falls keine Verschiebeoperation vorliegt, werden Listener befragt, ob das CatalogItem editiert werden darf. Wenn kein Listener ein Veto einlegt, wird das CatalogItem in mEditingItems gespeichert, Listener werden über das Editierbarmachen des CatalogItems informiert und das Item wird zurückgeliefert. Damit ist die get() Methode beendet.
Als nächstes wird überprüft, ob das CatalogItem in mItems vorkommt. Wenn ja, und falls es nicht editierbar sein soll, wird es einfach zurückgeliefert. Wird allerdings ein editierbares Item gewünscht, müssen Vorbereitungen getroffen werden, die ein späteres rollback() ermöglichen. Aus diesem Grund ist auch zu beachten, dass der DataBasket nicht null sein darf, sonst gibt es keine Möglichkeit für ein späteres rollback().
Zunächst werden Listener befragt, ob das CatalogItem editiert werden darf. Falls keine Einsprüche kommen, wird zunächst ein ShallowClone des CatalogItems erzeugt. Dazu wird die Methode getShallowClone() des CatalogItems verwendet, welche der Anwendungsentwickler selbst implementieren muss. Das originale CatalogItem wird aus mItems entfernt und in mTemporaryRemoved verschoben, also temporär entfernt, der ShallowClone wird in mTemporaryAdded und mEditingItems gespeichert, also temporär hinzugefügt und als editierbar gehandhabt. Anschließend werden noch zwei DataBasketEntries erzeugt, die dieses temporäre Hinzufügen und Entfernen festhalten. Aus dem originalen CatalogItem wird die Referenz auf den Katalog entfernt, für den ShallowClone wird eine Referenz auf den Katalog gesetzt. Anschließend werden Listener benachrichtigt, dass CatalogItems aus dem Katalog entfernt, zu ihm hinzugefügt und editierbar gemacht wurden. Dann wird der Modifikationszähler erhöht und der ShallowClone zurückgeliefert.
Alle Änderungen am CatalogItem betreffen nun den ShallowClone. Bei einem commit() wird das originale CatalogItem entfernt und der ShallowClone samt Änderungen wird das neue Original, bei einem rollback() wird der ShallowClone gelöscht und das Original wird zurück nach mItems kopiert.
Nachbetrachtungen
Im erwähnten Fall, dass das CatalogItem, welches geholt werden soll, in mTemporaryAdded steht, wird kein ShallowClone angelegt. Ist trotzdem ein commit() oder rollback() der Änderungen möglich? Die Antwort lautet natürlich ja. Werden Änderungen durchgeführt und mit einem commit() bestätigt, wird das temporär hinzugefügte und editierte CatalogItem nach mItems kopiert, bei einem rollback() einfach entfernt. Ein ShallowClone ist nicht nötig, da es kein Original im Catalog gibt, welches wiederhergestellt werden müsste.
Wenn ein CatalogItem editierbar gemacht wurde, werden zukünftige get() Aufrufe immer den ShallowClone zurückliefern. Dabei ist es egal, ob fForEdit true oder false ist. Die einzige Möglichkeit, das CatalogItem wieder nichteditierbar zu machen, ist ein commit() oder rollback() auszuführen.
Veto beim Editieren eines CatalogItems
Wenn CatalogItems editiert werden sollen, können CatalogChangeListener von Stocks oder Catalogs ein Veto einlegen. Vetos von Stocks kommen, wenn StockItems, die zum zu editierenden CatalogItem gehören, Transaktionen unterliegen, also gerade temporär hinzugefügt oder entfernt sind. Kataloge legen ein Veto ein, wenn sie einen ShallowClone haben. Dieser ist zu benutzen, um editierbare CatalogItems zu holen.
Namensänderungen von CatalogItems
SalesPoint bietet Interfaces und Implementierungen, die es ermöglichen, Namenskonventionen flexibel durchzusetzen. Diese sollen zunächst grob erläutert werden, da das Umbennnen von CatalogItems auf ihnen aufbaut.
Ein Objekt, welches einen Namen haben soll, der bestimmten Regeln genügt, muss das Interface Nameable implementieren. Die eigentlichen Regeln werden in einem Objekt, das das Interface NameContext implementiert, festgelegt. In der Methode setName(String sName, DataBasket db) des benennbaren Objekts, in der das eigentliche Umbenennen stattfindet, sollten folgende Schritte ablaufen:
- Überprüfung, ob der NameContext null ist.Der NameContext ist als Attribut im Nameable Objekt gespeichert.
- Falls ja, Zuweisung des neuen Namens. Es gibt keine Regeln, die eingehalten werden müssten.
- Andernfalls
- Aufruf der Methode checkNameChange(DataBasket db, String sOldName, String sNewName) des NameContext. In ihr wird überprüft, ob die gewünschte Namensänderung zulässig ist, d.h., diese Methode definiert die Regeln und überprüft ihre Einhaltung. Bei einem unzulässigen Namenswechsel wird mit einer NameContextException abgebrochen.
- Zuweisen des Namens.
- Aufruf der Methode nameHasChanged(DataBasket db, String sOldName, String sNewName) des NameContext. In dieser Methode können interne "Aufräumarbeiten" des NameContext stattfinden, falls sie nach einem Namenswechsel nötig werden.
Die Implementierung von Nameable wurde in der Klasse AbstractNameable vorgenommen. CatalogItemImpl, StockItemImpl und deren Unterklassen sind von AbstractNameable abgeleitet. Die Implementierung des NameContext Interfaces fand dementsprechend in CatalogImpl und StockImpl statt, welche ihren Items Namensänderungen erlauben oder verbieten müssen. Die Regel, die definiert wird, ist hauptsächlich, dass keine doppelten Namen vergeben werden dürfen. StockImpl verbietet übrigens grundsätzlich das Umbennen von StockItems, Namensänderungen gehen immer von CatalogItems aus. CatalogItems erhalten eine Referenz auf ihren NameContext über die Methode setCatalog(), also immer, wenn ihnen ein Elternkatalog zugewiesen wird.
checkNameChange()
Die Methode checkNameChange(DataBasket db, String sOldName, String sNewName) der Klasse CatalogImpl überprüft, ob das CatalogItem, dessen Name geändert werden soll, editierbar ist, ob der neue Name noch nicht vergeben ist und ob der richtige DataBasket übergeben wurde.
Im Detail passiert folgendes:
- Überprüfung, ob sich das CatalogItem in mEditingItems befindet, falls nicht, wird eine NameContextException geworfen, mit dem Hinweis, dass das CatalogItem vor der Namensänderung editierbar gemacht werden muss.
- Überprüfung, ob sich ein CatalogItem mit dem neu zu vergebenden Namen in mItems, mTemporaryAdded oder mTemporaryRemoved befindet. Falls ja, wird eine NameContextException geworfen, mit dem Hinweis, dass sich bereits ein CatalogItem mit dem entsprechenden Namen im Catalog befindet.
- Überprüfung, ob der übergebene DataBasket der selbe ist, mit dem das CatalogItem editierbar gemacht wurde. Falls nicht, wird eine NameContextException geworfen, mit dem Hinweis, dass der DataBasket keinen Eintrag für das umzubenennende CatalogItem enthält.
nameHasChanged()
Nach der eigentlichen Namensänderung wird nameHasChanged(DataBasket db, String sOldName, String sNewName) ausgeführt. Dabei werden alle umbenannten CatalogItems aus ihren jeweiligen Maps entfernt und mit einem neuen Key, nämlich dem neuen Namen, wieder eingefügt. Anschließend wird die Namensänderung in den DataBasket übernommen.
Im Detail läuft das folgendermaßen ab:
- Aus der Map mEditingItems wird das betroffene CatalogItem, welches noch mit dem Key sOldName abgespeichert ist, entfernt mit mit seinem neuen Namen als Key wieder hinzugefügt.
- Der selbe Vorgang läuft für mTemporaryAdded ab. In diesen beiden Maps steht der ShallowClone, der beim Editierbarmachen des CatalogItems erzeugt wurde. Das originale CatalogItem wird also nicht umbenannt, so dass ein rollback() auch eine Namensänderung rückgängig machen kann.
- Aktualisieren des DataBaskets. Beim Editierbarmachen des CatalogItems wurde ein DataBasketEntry angelegt, der das temporäre Hinzufügen des ShallowClones zum Stock repräsentiert. Dieser Entry wird ersetzt, und zwar durch einen Entry, der den neuen Namen des umbenannten CatalogItems als secondaryKey hat, ansonsten aber identisch ist.
Zum Schluss werden Listener benachrichtigt, dass der Name des CatalogItems geändert wurde.
Commit und Rollback
Wenn ein commit() oder rollback() auf einen DataBasket angewendet wird, resultiert das in einem commit() bzw. rollback() für jeden betroffenen DataBasketEntry (siehe DataBaskets). Wenn die betroffenen DataBasketEntries einen Catalog als Quelle oder Ziel haben, werden für diesen spezielle Methoden aufgerufen, welche die entsprechenden CatalogItems neu in die Maps mItems, mTemporaryAdded und mTemporaryRemoved einsortieren.
Commit
Wird auf einem DataBasketEntry ein commit() ausgeführt, so gibt es zwei Möglichkeiten, dies zu behandeln. War das zugehörige CatalogItem temporär hinzugefügt, so wird commitAdd() im Katalog ausgeführt, war das CatalogItem dagegen temporär entfernt, so resultiert das in einem commitRemove().
commitAdd()
Der Methode werden als Argumente der betroffene DataBasketEntry und der DataBasket, in welchem sich der Entry befindet, übergeben. Es wird zuerst überprüft, ob das destination Feld des DataBasketEntrys dem aktuellen Katalog entspricht. Dann wird das betroffene CatalogItem aus mTemporaryAdded entfernt und mItems hinzugefügt. Das CatalogItem bekommt eine Referenz auf den Katalog, der Modifikationszähler wird erhöht und Listener werden über das erfolgte commitAdd() informiert. Falls sich in mEditingItems ein CatalogItem mit gleichem Key befindet, so wurde eben der ShallowClone fest in den Katalog übernommen. Das originale CatalogItem wird aus mEditingItems gelöscht, dann werden Listener benachrichtigt, dass ein commit() auf das Editieren eines CatalogItems erfolgte. Zum Schluss wird das destination Feld des DataBasketEntrys auf null gesetzt.
commitRemove()
Mit dieser Methode wird ein CatalogItem aus mTemporaryRemoved entfernt. Es wird getestet, ob der Katalog im Feld source des DataBasketEntrys dem aktuellen Katalog entspricht. Dann wird das betroffene CatalogItem aus mTemporaryRemoved entfernt und die Referenz auf den Katalog wird gelöscht. Anschließend wird das source Feld des DataBasketEntrys auf null gesetzt und Listener werden über das erfolgte commitRemove() informiert.
Rollback
Wie beim commit gibt es auch beim rollback() zwei Möglichkeiten: Das rollback() des temporären Hinzufügens oder des temporären Entfernens. Die Methoden, die dafür aufgerufen werden, heißen entsprechend rollbackAdd() und rollbackRemove().
rollbackAdd()
Das Rückgängigmachen des temporären Hinzufügens eines CatalogItems beinhaltet das Entfernen des Items aus mTemporaryAdded inklusive Löschen der Referenz auf den Katalog. Es findet ein Update des DataBasketEntrys statt, sein source Feld wird auf null gesetzt. Danach werden der Modifikationszähler erhöht und Listener über das rollbackAdd() informiert. Falls das CatalogItem editierbar war, wird es aus mEditingItems entfernt und Listener werden benachrichtigt, dass ein rollback() auf das Editieren des Items stattgefunden hat.
rollbackRemove()
Es wird das temporäre Entfernen eines CatalogItems rückgängig gemacht. Dazu wird das betroffene Item aus mTemporaryRemoved entfernt und mItems hinzugefügt. Das CatalogItem erhält eine Referenz auf den Katalog, der Modifikationszähler wird erhöht und Listener werden über das erfolgte rollbackRemove() informiert. Zuletzt wird der DataBasketEntry aktualisiert, sein source Feld wird zu null.
Geschachtelte Kataloge
Da Catalogs von CatalogItems abgeleitet sind, ist es möglich, Kataloge zu schachteln (Entwurfsmuster Composite). Schachtelung ist allerdings nur bei nicht-persistenten Katalogen möglich. Um Unterkatalogen CatalogItems hinzufügen zu können, müssen sie erst editierbar gemacht werden. Das lässt sich problemlos mit der get() Methode erreichen (siehe Holen eines CatalogItems). Dabei wird, wie bei normalen CatalogItems, mit einem ShallowClone des Catalogs gearbeitet.
Kopie eines Catalogs erzeugen
Kopien eines Catalogs bestehen standardmäßig aus einem CatalogImpl mit dem gleichen Namen wie das Original und beinhalten die selben CatalogItems. Der Ausgangspunkt für das Erzeugen einer Kopie ist die get() Methode, von der aus getShallowClone() aufgerufen wird. Diese macht folgendes:
- Erzeugen eines neuen leeren Katalogs mit der Hook-Methode createPeer().
- Kopieren der aktuellen Maps mItems, mTemporaryAdded, mTemporaryRemoved und mEditingItems in den neuen Katalog. Dabei werden zwar neue Maps angelegt, ihre Inhalte verweisen aber auf die selben CatalogItem Objekte wie die originalen Maps.
- Hinzufügen von Listenern:
- Der originale Catalog wird als Listener beim ShallowClone angemeldet.
- Der Elternkatalog, falls vorhanden, wird als Listener beim ShallowClone angemeldet.
- Der ShallowClone wird zurückgeliefert.
Die Methode getShallowClone() liefert standardmäßig einen Katalog vom Typ CatalogImpl. Es kann vorkommen, dass man einen speziellen Katalog benutzt (eine Unterklasse von CatalogImpl), dieser muss auch für den ShallowClone verwendet werden. Allerdings sollte nicht getShallowClone überschrieben werden, da dort Methoden zur Anwendung kommen, die der Datenintegrität dienen. Zudem sind sie als private deklariert, so dass man sie nicht einfach selbst aufrufen kann. Um seinen eigenen Katalog für den ShallowClone bereitzustellen, überschreibt man stattdessen die Hook-Methode createPeer(). Diese wird von getShallowClone() aufgerufen, um die leere Kopie des Katalogs zu erzeugen.
Tief geschachtelte Kataloge
Bei tieferen Schachtelungen können die Kataloge nicht sofort editierbar gemacht werden, weil auch ihre Elternkataloge in einem Katalog stecken und deshalb selbst noch nicht editierbar sind.
Mit der Methode getEditableCopy(DataBasket db) werden rekursiv alle Elternkataloge editierbar gemacht, bis der gewünschte Katalog editierbar ist. Anschließend liefert sie eine editierbare Kopie des Katalogs zurück. Im Detail passiert Folgendes:
- Überprüfung, ob der Katalog editierbar ist
- Wenn ja, wird er zurückgeliefert.
- Andernfalls wird getEditableCopy() für den Elternkatalog ausgeführt. Sobald ein Elternkatalog editierbar ist, wird die Rekursion unterbrochen und es wird über die get() Methode für CatalogItems eine editierbare Kopie des aktuellen Katalogs zurückgeliefert.
Der Modifikationszähler
Bei tieferen Schachtelungen können die Kataloge nicht sofort editierbar gemacht werden, weil auch ihre Elternkataloge in einem Katalog stecken und deshalb selbst noch nicht editierbar sind.
Mit der Methode getEditableCopy(DataBasket db) werden rekursiv alle Elternkataloge editierbar gemacht, bis der gewünschte Katalog editierbar ist. Anschließend liefert sie eine editierbare Kopie des Katalogs zurück. Im Detail passiert Folgendes:
- Überprüfung, ob der Katalog editierbar ist
- Wenn ja, wird er zurückgeliefert.
- Andernfalls wird getEditableCopy() für den Elternkatalog ausgeführt. Sobald ein Elternkatalog editierbar ist, wird die Rekursion unterbrochen und es wird über die get() Methode für CatalogItems eine editierbare Kopie des aktuellen Katalogs zurückgeliefert.
Salespoint Desktop
Allgemeines
Interaktion wird in SalesPoint-Anwendungen mit Hilfe von Prozessen realisiert. Prozesse benötigen eine Umgebung, in der sie laufen können, den sogenannten ProcessContext. Im Framework gibt es zwei dieser Kontexte, nämlich SalesPoints und Shop.ProcessHandle, eine innere Klasse im Shop, welche für Hintergrundarbeiten gedacht ist.
Ein Prozess ist wie ein Automat aufgebaut, er besteht aus Zuständen, Gates genannt, und Übergängen, den Transitions. Gates sind für länger andauernde Aktionen gedacht, zum Beispiel Nutzerinteraktion. Deshalb gibt es auch eine spezielle Klasse UIGate, welche FormSheets, also die Nutzungsoberflächen, anzeigen kann. Ein Prozess kann an einem Gate unterbrochen werden.
Transitionen dienen dem Wechsel der Gates. Sie sind Zustandsübergänge. Zusätzlich können selbst definierte Aktionen ausgeführt werden, zum Beispiel das Verarbeiten der eingegebenen Daten in einem FormSheet. Prozesse können während einer Transition nicht unterbrochen werden.
Details
Start und Ablauf eines Prozesses
Vorbereitungen zum Prozessstart
Ein Prozess auf einem SalesPoint wird gestartet, indem die Methode runProcess() dieses SalesPoints mit dem zu startenden Prozess p als Parameter aufgerufen wird. Falls gerade ein Prozess läuft, wird er gesichert. Der SalesPoint bekommt eine Referenz auf den neuen Prozess, der Prozess eine Referenz auf den SalesPoint und dessen DataBasket. Danach schließt sich der eigentliche Prozessstart mit p.start() an.
Falls der zu startende Prozess ein Hintergrundprozess sein soll, wird dieser mittels der runProcess() Methode des Shops gestartet. Es wird zunächst ein neuer ProcessHandle erzeugt. Im Konstruktor des ProcessHandle werden wie in der runProcess() Methode des SalesPoints Referenzen zwischen Prozess und Handle gesetzt. Anschließend wird der Handle in einer Liste aller ProcessHandles, die vom Shop verwaltet wird, abgespeichert. Falls der Shop nicht gerade angehalten ist, wird der Prozess mit start() gestartet, andernfalls gestoppt mit suspend().
Prozessstart
Falls nicht noch ein Prozess läuft, werden zunächst einige Initialisierungen vorgenommen. Das Flag fSuspended, welches eine Prozessunterbrechung anzeigt, wird auf false gesetzt, da der Prozess anlaufen soll und nicht mehr unterbrochen sein kann. Falls der Prozess neu ist, also gar nicht unterbrochen war, wird zusätzlich einer Variablen gCurGate das Startgate mit Hilfe von getInitialGate() des Prozesses zugewiesen. Ein Prozess wird gerade fortgesetzt, wenn das Flag fResumed true ist, ansonsten handelt es sich um einen frisch gestarteten. Es wird anschließend ein Thread trdMain erzeugt und gestartet, welcher die Methode main() ausführt.
Auch hier finden zu Beginn kleinere Initialisierungen statt. Falls der Prozess gerade neu startet, also nicht nur fortgesetzt wird, wird die Methode processStarted() des Prozesskontextes ausgeführt. Somit kann jeder Prozesskontext angemessen auf einen Prozessstart reagieren. Falls der Kontext ein SalesPoint ist, werden dessen Form- und Menusheet entfernt, außerdem meldet sich der SalesPoint, der bislang als Listener am Display registriert war, ab. Ist der Kontext ein Shop.ProcessHandle, also ein Kontext für Hintergrundprozesse, werden beim Aufruf von processStarted() keinerlei Aktionen ausgeführt. Als nächstes wird onResumeOrStart() aufgerufen. Dabei handelt es sich um eine Hook-Methode, die vom Anwendungsprogrammierer zu überschreiben ist, falls spezieller Initialisierungscode beim Start eines Prozesses benötigt wird. Tritt in dieser Methode ein Fehler auf, wird gCurGate ein Fehlergate zugewiesen.
Die Prozessschleife
Es folgt der Eintritt in die eigentliche Kontrollschleife. Diese wird so lange ausgeführt, wie curGate nicht null und fSuspended false ist, der Prozess also nicht unterbrochen oder beendet wurde.
Es wird ein SaleProcess$SubProcess trdGate, im Folgenden GateThread genannt, erzeugt und ausgeführt. Ein SaleProcess$SubProcess ist eine Unterklasse von Thread, die sich von diesem nur dadurch unterscheidet, dass sie beim Ausführen spezielle Throwables vom Typ ProcessErrorError, welche für die prozessinterne Fehlerbehandlung benutzt werden, abfängt. Der GateThread hat die Aufgabe, eine Transition zurückzuliefern und der Variablen tCurTransition zuzuweisen. Das geschieht durch den Aufruf der Methode getNextTransition() von gCurGate. Falls der Prozess unterbrochen wird, bevor eine Transition zurückgeliefert wurde (fSuspended ist true), wird tCurTransition zu null und die Hauptkontrollschleife verlassen.
Sobald der GateThread beendet ist, wird ein SaleProcess$SubProcess trdTransition, der TransitionThread, erzeugt und ausgeführt. Dieser liefert das nächste Gate zurück, zu dem gesprungen werden soll, indem tCurTransition.perform() aufgerufen wird. Dieses Gate wird in gCurGate gespeichert. Falls versucht wird, den Prozess zu unterbrechen, solange die Transition noch im Gange ist, wird dies ignoriert. Nachdem der TransitionThread seine Aufgabe beendet hat und ein Zielgate gefunden wurde, wird onSuspended() aufgerufen, eine leere Hook-Methode wie die bereits erwähnte Methode onResumeOrStart(). Falls der Prozess nicht unterbrochen oder beendet wurde, wird die Hauptkontrollschleife nun erneut abgearbeitet.
Prozessende
Nach verlassen der Prozessschleife wird die Hook-Methode onFinished() aufgerufen. Falls der Prozess richtig beendet wurde, also nicht nur unterbrochen ist, wird zusätzlich pcContext.processFinished() ausgeführt. Ist der Prozesskontext ein SalesPoint, wird bei Ausführung von processFinished() nachgesehen, ob es noch inaktive Prozesse gibt, die vom eben abgearbeiteten verdrängt worden sind. Wenn dem so ist, wird der aktuellste von ihnen zum aktiven Prozess des SalesPoints gemacht und fortgesetzt. Andernfalls wird null als aktueller Prozess gespeichert und es werden die Änderungen, die beim Prozessstart von processStarted() gemacht wurden, rückgängig gemacht. Der SalesPoint meldet sich wieder als FormSheetListener am Display an, außerdem werden Form- und MenuSheet des SalesPoints gesetzt.
Wird processFinished() eines ProcessHandles aufgerufen, wird die Referenz des beendeten Prozesses auf den Handle entfernt sowie der Handle aus der Liste der ProcessHandles im Shop gelöscht.
Gates am Beispiel UIGate
Wie bei der Beschreibung der Prozessschleife erwähnt, ist ein Prozess hauptsächlich ein Wechsel zwischen Gates und Transitionen. Die meistgenutzten Gates sind die UIGates. Diese haben die besondere Eigenschaft, dass an ihnen Form- und MenuSheets angezeigt werden können. Damit ist es möglich, dass der Nutzer durch seine Eingaben aktiv den Verlauf des Prozesses bestimmt.
Jedes Gate besitzt die Methode getNextTransition(), mit der der steuernde Prozess in seiner Kontrollschleife die nächste auszuführende Transition erfragt. Beim UIGate kann es beliebig lange dauern, bis diese Transition feststeht, da sie durch die Nutzereingaben bestimmt und ausgelöst wird. Die getNextTransition() Methode besteht deshalb hauptsächlich aus einer Schleife, die erst verlassen wird, wenn tatsächlich die auszuführende Transition feststeht.
Das UIGate besitzt ein Integerflag nChanged, welches je nach Zustand anzeigt, ob ein FormSheet oder MenuSheet gesetzt wurde, eine Transition bestimmt wurde oder ob sich nichts geändert hat. Beim Start von getNextTransition() wird zunächst die auszuführende Transition mit null initialisiert. Außerdem wird in nChanged festgehalten, dass sich FormSheet und MenuSheet geändert haben. Der Grund ist, dass im Konstruktor von UIGate ein FormSheet und ein MenuSheet übergeben werden, welche zum Zeitpunkt des Methodenaufrufs getNextTransition() jedoch noch nicht angezeigt wurden.
Anschließend wird eine Schleife betreten, die solange ausgeführt wird, wie nChanged anzeigt, dass noch keine Transition festgelegt wurde. Falls nChanged anzeigt, dass sich das Form- und/oder MenuSheet geändert hat, wird setFormSheet() bzw. setMenuSheet() des aktuellen Prozesskontextes aufgerufen und die Sheets werden gesetzt. Anschließdend wird nChanged auf NOTHING gesetzt. Damit ist der Schleifenkörper abgearbeitet und ein neuer Durchlauf könnte beginnen. Allerdings würde die Schleife sehr oft durchlaufen, ohne dass sich überhaupt etwas verändert hat. Deshalb wird der aktuelle Thread schlafengelegt und erst wieder aufgeweckt, wenn irgend eine Änderung stattfindet, welche einen erneuten Schleifendurchlauf rechtfertigt.
Setzen von FormSheets, MenuSheets und Transitionen
Die Klasse UIGate besitzt die Methoden setFormSheet(), setMenuSheet() und setNextTransition(). Diese Methoden arbeiten nach dem gleichen Prinzip. Zunächst wird das zu ändernde Attribut, FormSheet, MenuSheet oder Transition, in einer lokalen Variablen gespeichert. Danach wird in nChanged eingetragen, dass sich das entsprechende Attribut geändert hat. Anschließend wird der Thread, der am Ende der Schleife von getNextTransition() schlafengelegt wurde, wieder aufgeweckt. Falls keine Transition gesetzt wurde, ist das Resultat ein erneuter Schleifendurchlauf, in dem Form- und MenuSheet nach Bedarf aktualisiert werden. Falls aber eine Transition gesetzt wurde, wird die Schleife verlassen und die Transition zurückgeliefert. Damit geht die Kontrolle zurück an die Prozessschleife des SaleProcess.
Transitionen
Was für Gates die Methode getNextTransition() ist, ist für Transitionen perform(). Damit wird der Prozessschleife das Gate zurückgeliefert, zu dem gesprungen werden soll. Innerhalb der perform() Methode kann bei Bedarf beliebiger Code ausgeführt werden. Allerdings sollte sich der Entwickler an das Prozesskonzept halten und keine Nutzerinteraktion in der Transition implementieren.
Salespoint Web
Seit der Version 2010 stellt Salespoint die Möglichkeit sowie Hilfsmittel bereit, in einem ServletContainer (vorrangig Apache Tomcat) ausgeführt zu werden.
Allgemeines
Basierend auf dem etablierten, quelloffenen Framework Spring (in der Version 3), dessen MVC-Umsetzung das Request/Response-Handling kapselt und die Entwicklung einer Webapplikation stark vereinfacht. Die komplette Salespoint Daten- und Nutzerverwaltung mit integrierte Persistenz kann auf gleiche, bisher beschriebene Weise genutzt werden. Wie Spring funktioniert kann einerseits in den Web-Tutorials sowie in der sehr anschaulichen und umfangreichen Dokumentation von Spring selbst nachgelesen werden. Im Folgenden wird auf die von Salespoint bereitgestellte TagLibary zur Visualisierung von Datenbeständen sowie einiger Kontrollstrukturen näher eingegangen.
TagLibary
In eine JSP-Datei lässt sich die TagLibary mit folgendem Befehl einbinden:
<%@ taglib uri="http://salespointframework.org/web/taglib" prefix="sp" %>
View
View-Tags dienen dem Rendern von Katalogen und Beständen. Diese werden in ein AbstractTabelModel (ATM), welches den Katalog bzw Bestand in tabellarischer beschreibt, überführt und dem ViewTag übergeben, welcher daraus eine entsprechende Repräsentation in HTML rendert. Es gibt 3 einfach zu benutzende konkrete ViewTags:
- Table
- rendert eine native HTML-Tabelle mit HTML-Tags wie <table>,<tr>,<td>,...
- CssTable
- rendert eine CSS-Tabelle mit HTML-Tags wie <div style="display:table;">,<div style="display:table-row;">,<div style="display:table-cell;">,...
- List
- rendert eine CSS- mit HTML-Tags wie <div style="display:table;">,<div style="display:table-row;">,<div style="display:table-cell;">,...
Attribute
Name | Notwendig | Defaultwert | Beschreibung |
---|---|---|---|
abstractTableModel | X | ATM des zu rendernden Datenbestandes | |
id | leer | entspricht dem HTML-id-Attribut | |
styleName | leer | entspricht dem HTML-class-Attribut zur Zuweisung von CSS-Klassen | |
style | leer | entspricht dem HTML-style-Attribut zur Inline-CSS-Definition | |
searchField | false | true, wenn ein Sucheingabefeld zum Filtern eingeblendet werden soll. Achtung: SearchFieldInterceptor aktivieren! TODO LINK ZUM FAQ SETZEN | |
searchString | leer | Zeichenkette, nach der der Bestand gefiltert werden soll. Funktioniert unabhängig von der Einblendung des Sucheingabefeldes. | |
extraCols | leer | Liste mit ExtraColumns für den Datenbestand TODO LINK ZUM FAQ SETZEN | |
positionOfExtraColumns | BACK | Position der ExtraColumns | |
writeSummaryAhead | leer | TODO UWE, DOKU HIER! |
Für fortgeschrittende Nutzung stehen 2 weitere ViewTags zur Verfügung:
- View
- allg. ViewRenderer, dem zusätzlich eine IHtmlViewRepresentation übergeben wird. Sinnvoll, wenn man eine eigene Html-Repräsentation definieren will.
- CompletedView
- rendert eine übergebene View-Klasse. Sinnvoll, wenn man eine eigene View definieren will.
DoubleView
Der DoubleViewTag ist die Oberflächenkomponente neben dem DoubleViewController und stellt die Möglichkeit bereit, den Benutzer Elemente von einem Katalog bzw. Bestand in einen anderen verschieben zu lassen. Wie dieses Zusammenwirken funktioniert, wird hier beschrieben.
Der DoubleViewTag beinhaltet 2 ViewTags und kann auf 2 verschiedene Weisen eingesetzt werden:
- Views als Attribute
<sp:DoubleView sourceView="${sourceATM}" destinationView="${destATM}" />
- Views als InnerTag
<sp:DoubleView>
<sp:Table abstractTableModel="${sourceATM}" />
<sp:Table abstractTableModel="${destATM}" />
</sp:DoubleView>
Die 2 Variante wird empfohlen, da sich damit die individuellen ViewTags komplett und wie die einzeln Verwendeten konfigurieren lassen. Achtung: Es müssen exakt 2 ViewTags angegeben werden - dies können auch unterschiedliche ViewTags sein. Attribute:
Name | Notwendig | Defaultwert | Beschreibung |
---|---|---|---|
sourceView | null | ATM des zu rendernden Quelldatenbestandes | |
destinationView | null | ATM des zu rendernden Zieldatenbestandes | |
showNumberField | false | true, wenn ein Eingabefeld für die Anzahl eingeblendet werden soll | |
id | leer | entspricht dem HTML-id-Attribut | |
styleName | leer | entspricht dem HTML-class-Attribut zur Zuweisung von CSS-Klassen |
Achtung: Der DoubleViewTag kann nur in Zusammenwirken mit dem DoubleViewController sinnvoll eingesetzt werden!
LoginDialog
Der LoginDialogTag rendert ein einfaches LoginFormular.
Attribute:
Name | Notwendig | Defaultwert | Beschreibung |
---|---|---|---|
showUsers | true | false, wenn statt der SelectBox mit registrierten Nutzernamen ein leeres Eingabefeld angezeigt werden soll | |
id | leer | entspricht dem HTML-id-Attribut | |
styleName | leer | entspricht dem HTML-class-Attribut zur Zuweisung von CSS-Klassen |
Achtung: LoginInterceptor notwendig!
LoggedIn
Der LoggedInTag ist eine Kontrollstruktur mitdessen Hilfe man Teile einer JSP-Seite anhand des Loginstatus aus- bzw. einblenden kann.
Beispiel:<sp:LoggedIn status="false">
<sp:LoginDialog />
</sp:LoggedIn>
Im Beispiel wird das LoginFormular nur dann angezeigt, wenn der diese Seite aufrufende Benutzer nicht eingeloggt ist.
Attribute:
Name | Notwendig | Defaultwert | Beschreibung |
---|---|---|---|
status | X | true: alle inneren Tags werden gerendert, wenn der Nutzer eingeloggt ist.
false: alle inneren Tags werden gerendert, wenn der Nutzer ausgeloggt ist. |