Innerhalb von JAXForms existiert ein generisches universelles Konstrukt zum Transformieren von Quellen auf Senken basierend auf dem ETL-Prinzip (Extract-Transform-Load).
- universell einsetzbar
- einfach Anwendbar
- offen für Erweiterungen (OCP)
- sauberes OO Design
- Typsicher
- DB Mapping Support (Mixed Mode)
- XML Konfiguration
- Multi Source Multi Sink (n:m Tranformation Erweiterung bis anhin war 1:1 beschränkt (2:1) möglich)
- Performant
- Abwärtskompatibel
- Testbar mit guter Testabdeckung
- Dokumentiert
- DOM → DOM
- W3C Document → DOM
- DOM → W3C Document
- W3C Document → W3C Document
- JSONObject → JSONObject
- CSV → XML
- Java Object
- Input
- Transformationskontext (erstellt aus der Transformationskonfiguration)
- Transformer
- Output
- optional Context DOM
- die Konfiguration besteht aus folgenden Elementen properties, converter, simple, group, list
- Transformationsregeln (simple, group, list)
- die Transformation läuft strikt sequentiell wie in der Transformationskonfiguration hinerlegt ab
- simple - Mapping von einfach Werten
- group - Bildung von logischen Transformationsgruppen (z.B. ganzer XML-Ast)
- list - Mapping von Listen (atomar oder spezifisch mittels Unteranweisungen)
Für jede Quelle muss ein entsprechender Extraktor (Leser) vorhanden sein resp. für jede Senke muss ein entsprechner Loader (Schreiber) verfügbar sein.
Der Extraktor weis wie er anhand der Quellpfade aus der Transformationskonfiguration die Daten aus der Quelle extrahiert, im Normalfall wird dies durch eine entsprechende Path-Engine ermöglicht.
Der Loader weis wie er anhand der Zielpfade aus der Transformationskonfiguration die Daten auf die Senke schreibt, im Normalfall wird dies auch hier durch eine entsprechende Path-Engine ermöglicht.
Die Transformationslogik ist universell und somit unabhängig von Quelle und Senke. Die Extraktoren, Loader und Transformationslogik sind zustandsunabhängig, der Zustand wird ausschliesslich im Transformationskontext geführt.
Der Transformationkontekt enthält die durchzuführenden Transformationsschritte und auch den Evaluationskontext, welcher für die Ausführung der JEP-Ausdrücke verwendet wird.
Aufgrund der Möglichkeit für den Einsatz von JEP-Formeln ist es Möglich während der Transformation auch auf die Quelle zu schreiben.
Werden mehrere Quellen resp. Senke für eine Transformation verwendet, so können diese durch die Vergabe einer sourceId sowie targetId eindeutig identifiziert werden. Dadurch ist es möglich auch aus mehreren heterogenen Quellen resp. Senken eine Transformation durchzuführen.
Was als Quelle und/oder Senke verwendet wird ist offen, es braucht einzig ein entsprechende Loader resp. Extraktor.
Die Anwendung innerhalb JAXForms erfolgt wie folgt:
try {
final TransformationContext transformationContext = TransformationContext.create(user, "resources/LI-OMS/transform2fullsearch.xml");
Transformer.with(transformationContext)
.addSource(orderDom, new DomExtractor(new NativeTypeValueProvider()))
.addTarget(fullSearchDom, new DomLoader(new NativeTypeValueProvider()))
.transform();
} catch (final Exception e) {
LogRegistry.getInstance().error(getClass(), "transformation failed: " + e.getMessage());
}
Standalone (z.B. UnitTest) könnte derselbe Aufruf wie folgt aussehen:
try {
final Document transformDom = DomBuilder.withSchemaFilePath("path/to/transformation.xsd").build("<transformation>...</transformation>");
final TransformationContext transformationContext = TransformationContext.create(transformDom);
Transformer.with(transformationContext)
.addSource(orderDom, new DomExtractor(new NativeTypeValueProvider()))
.addTarget(fullSearchDom, new DomLoader(new NativeTypeValueProvider()))
.transform();
} catch (final Exception e) {
LogRegistry.getInstance().error(getClass(), "transformation failed: " + e.getMessage());
}
Convention over Configuration, es wird immer versucht ein optimales Staddardverhalten zu bieten, so dass die Transformationskonfiguration möglichst schlank gehalten werden kann.
Bei der Angabe von Formeln kann zusätzlich noch der Evaluationskontext (keinen, Standardkontext, Source, Target) angeben werden. Wird nichts spezifiziert, wird automatisch der Standardkontext bei der Evaluation verwendet.
Kann die Formel nicht fehlerfrei evaluiert werden wird die Transformationsregel nicht ausgeführt.
Jede Transformationsregel (simple, group, list) kann dynamisch aktiviert bzw. deaktiviert werden. Dazu stehen zwei Mechanismen zur Verfügung:
Der Condition stellt eine performante Vereinfachung für das enabled dar und bietet folgende Möglichkeiten:
- never - wird nie ausgeführt
- always - wird immer ausgeführt unabhängig von sourceValue und targetValue (Standardverhalten)
- bothAbsent - wird nur ausgeführt wenn sourceValue == null && targetValue == null ist
- bothPresent - wird nur ausgeführt wenn sourceValue != null && targetValue != null ist
- targetAbsent - wird nur ausgeführt wenn taretValue == null ist
- targetPresent - wird nur ausgeführt wenn targetValue != null ist
- sourceAbsent - wird nur ausgeführt wenn sourceValue == null ist
- sourcePresent - wird nur ausgeführt wenn sourceValue != null ist
Oder mittels enabled, wo eine Boolscher-Formelausdruck mitgegeben werden kann.
Wird die Condition nicht spezifiziert wird always als Standardverhalten angewendet.
Beide lassen sich auch kombinieren, wobei zuerst die Condition und anschliessend die enabled Formula ausgewertet wird.
Die condition wird vererbt, das heisst untergeordnete Konfiguraitionselemente erben die condition, wenn eine anderes Verhalten gewünscht ist muss dieses explizit angegeben werden.
Der AddMode spezifiziert wie der jeweilige Wert hinzugefügt werden soll, der AddMode wird hauptsächlich für Listen verwendet.
- clear - löscht den vorhanden Wert resp. Liste
- additive - fügt den Wert hinzu
- overwrite - Überschreibt den vorhanden Wert
- ignore - der Wert wird nicht auf das Target geschrieben (kann reine Formelausführungen oder reine Alias Definitionen verwendet werden)
Der addMode ignore wird zum Definieren von alias verwendet, welche nicht direkt auf das Target geschrieben werden sollen.
Sämtliche angegebenen Properties (transformation.xml, programmatisch) und alias stehen als Variablen bei der Formelauswertung und zur Substitution zur Verfügung.
Das heisst Variablen können in Pfaden, Formeln oder default-Angabe verwendet werden. Dazu müssen die Property-Namen mittels ${myVar} escaped werden.
Standardmässig sind immer folgende Variablen:
- ${NULL} für den Wert
null
- ${SOURCE_VALUE} der aktuelle Source Value
- ${TARGET_VALUE} der aktuelle Target Value
- ${SOURCE_PATH} der aktuelle Source Pfad
- ${TARGET_PATH} der aktuelle Target Pfad
- ${SOURCE_INDEX}, ${TARGET_INDEX} mittels Z-Notation kann auch auf äussere Indexe zugegriffen werden z.B. ${SOURCE_INDEX-1}, ${TARGET_INDEX-2}
Mittels as kann jedes beliebige Transformationsresultat in die Transformations-Properties geschrieben werden. So dass diese Aliase in den Formeln und für die Substitution verwendet werden können.
<transform>
<simple source="//orderType" target="/FullSearchOrder/orderType" converter="surveillanceMeasureType" as="orderType"/>
<simple target="/FullSearchOrder/order/type" formula="orderType"/>
<simple source="//targetType" target="/FullSearchOrder/order/targetType" as="targetType"/>
<simple source="//targetType" target="/FullSearchOrder/order/type/${orderType}/fieldSelection" />
<simple source="//targetId" target="/FullSearchOrder/order/type/${orderType}/${targetType}" />
</transform>
- Transformationsschritte: 1. Formel 2. Converter 3. Default 4. Substitution 5. Evaluation context aktualisieren (Fehlt die Konfiguration wird der entsprechende Schritt übersprungen)
- wird nur der Zielpfad (target) angegeben, bleibt der Quellpfad undefiniert und wird als
Condition#sourceAbsent
gewertet. - wird nur der Quellpfad (source) angegeben, wird dieser implizit auch für den Zielkpfad (target) verwendet, dies erspart die Redundante Pfadangabe bei homogenen Transformationen (z.B. XML → XML)
- der Fallbackwert (default) wird gesetzt, wenn kein sourceValue, kein Formel (formula), kein Formelresultat und kein Konverter resp. kein Konverterresultat vorliegt.
- mittels as kann das Transformationsresultat als Variable für die Formelaustwertung verendet werden
Event | Auslöseereignis | Anmerkungen |
---|
clearList | List Transformation Rule mit addMode=clear | Liste löschen |
setList | List Transformation Rule ohne Untertransformationen | Verwendung: ganze Liste kopieren |
beforeList | List Transforamtion Rule mit Untertransformationen | Anlegen einer Datenstruktur auf dem Target |
afterList | List Transforamtion Rule mit Untertransformationen |
|
beforeListElement | List Transforamtion Rule mit Untertransformationen wird bedingungslos vor jedem Listen Element der Source aufgerufen (ungeachtet ob die Transformationsregeln angewendet werden oder nicht) | Anlegen eines neuen Listeneintrages (Es ist nicht möglich vorgängig zu Prüfen ob eine oder mehrere Transformation Rules innerhalb des aktuellen Listenelementes aktiv sind, da diese Zustandsbehaftet formuliert werden können.) |
afterListElement | List Transforamtion Rule mit Untertransformationen wird bedingungslos nach jedem Listen Element der Source aufgerufen (ungeachtet ob die Transformationsregeln angewendet worden sind oder nicht) | Abräumen (Falls keine Transformation für dieses Listenelement stattgefunden hat, kann hier das angelegte Listenelement wieder entfernt werden.) |
Szenarios:
List ohne Subtransformation: [clearList] setList
Leere Liste mit Subtransformation: [clearList] beforeList afterList
Liste mit Subtransformation mit zwei Element [clearList] beforeList beforeListElement [Subtransformation Events] afterListElement beforeListElement [Subtransformation Events] afterListElement afterList
Die Gruppe kann verwendet werden um mehrere Transformationsanweisungen zusammenzufassen. Sei dies für das dynamische Umschalten oder für redundante Pfade oder sonstige Eigenschaften.
Der Converter dient zum konvertieren (mappen) von Werten. Die Logik ist kaskadiert, zuerst wird ein 1:1 mapping versucht anschliessen ein Regex-Matching, falls nichts zutrifft wird der Standardwert zurückgeliefert.
- wird keine default angegeben und kein Match vorliegt, wird der Input 1:1 durchgereicht (transparent)
- wird default="${NULL}" gesetzt wird bei keinem Match der Wert
null
zurückgeliefert - die Konvertierung kann Vorwärts (default, converterMode="fromTo") oder Rückwärts erfolgen (converterMode="toFrom")
- beim Converter-Mode fromTo werden nur entry und from berücksichtigt bei toFrom werden nur entry und to Einträge berücksichtigt
- die Konvertierung kann symetrisch mittels entry erfolgen (hier können jedoch keine Regulären Ausdrücke verwendet werden)
- oder asysmetrisch mittels from oder to mit der Möglichkeit zur angabe von Regulären Ausdrücken
- die Converter-Einträge entry, from, to können beliebig miteinander kombiniert werden die Reihenfolge bei den entry ist egal, hingegen spielt die Reihenfolge der Regex-Pattern eine Rollge zuerst spezifisch und anschliessend die generellen Matches
- Standardmässig wird Gross-Kleinschreibung nicht beachtet (ignoreCase="true"), mittels ignoreCase="false" kann die Konvertierung case sensitiv erfolgen
<converter id="surveillanceMeasureType" default="">
<entry from="HD_28_NA" to "naFull"/>
<from regex=".*_IP" to="ipFull"/>
<from regex=".*_NAT" to="natFull"/>
<from regex=".*_NA" to="naFull"/>
<from regex=".*_TEL" to="telFull"/>
<from regex=".*_EMAIL" to="emailFull"/>
</converter>
- n:m Transformation (mehrere Quellen auf mehere Senken)
- Path-Engine wie z.B. XPath oder QL (Query Language) wie z.B. SQL
- Spezifischer Extractor
- Spezifischer Loader
- Identifizieren der Soruce / Target
- Erweiterungen
- DomExtractor
- DocumentExtractor (W3C Document)
- JsonExtractor
- CsvExtractor
- POJO (Java Object)
- DomLoader
- DocumentLoader
- JsonLoader
- MapLoader
- StringBuilderLoader
- DatabaseLoader
<?xml version="1.0" encoding="UTF-8"?>
<transform>
<list sourceList="/Record/Import/Category[@Name='Zuweiser-Import']" targetList="ksbl_person">
<simple default="ZUWEISER" target="CATEGORY" />
<simple source="./Field[@FieldName='Person.Personnummer']" target="PERS_NR" />
<simple source="./Field[@FieldName='Person.Mandant']" target="MANDANT" />
<simple source="./Field[@FieldName='Person.Nachname']" target="LASTNAME" as="LASTNAME" />
<simple source="./Field[@FieldName='Person.Vorname']" target="FIRSTNAME" as="FIRSTNAME" />
<simple source="./Field[@FieldName='Person.AnredeText']" target="SALUTATION" />
<simple source="./Field[@FieldName='Person.TitelText']" formula="evaluateTitle(SOURCE_VALUE)" target="TITLE" as="TITLE" />
<simple source="./Field[@FieldName='Person.Telefon1']" target="TEL_1" />
<simple source="./Field[@FieldName='Person.Telefon2']" target="TEL_2" />
<simple source="./Field[@FieldName='Person.Handy1']" target="MOBILE_1" />
<simple source="./Field[@FieldName='Person.Handy2']" target="MOBILE_2" />
<simple source="./Field[@FieldName='Person.Fax']" target="FAX" />
<simple source="./Field[@FieldName='Person.EMail']" target="EMAIL_1" />
<simple source="./Field[@FieldName='Person.EMailHIN1']" target="EMAIL_2" />
<simple source="./Field[@FieldName='Person.EMailHIN2']" target="EMAIL_3" />
<simple source="./Field[@FieldName='Person.Versandart']" target="SHIPPING_METHOD" />
<simple source="./Field[@FieldName='Person.Rolle']" target="ROLE" converter="hospisRole" />
<simple source="./Field[@FieldName='Person.RolleGueltigVon']" formula="getFormattedDate("yyyymmdd","yyyy-MM-dd",SOURCE_VALUE)" target="WORK_BEGIN_DATE" />
<simple source="./Field[@FieldName='Person.RolleGueltigBis']" formula="getFormattedDate("yyyymmdd","yyyy-MM-dd",SOURCE_VALUE)" target="WORK_END_DATE" />
<simple formula="getFormattedDate("yyyy-MM-dd HH:mm:ss")" target="PROCESSING_DATETIME" />
<simple formula="if(stringLength(FIRSTNAME) == 0 || trim(FIRSTNAME) == ".", LASTNAME, if(stringLength(TITLE) == 0, FIRSTNAME + " " + LASTNAME, TITLE + " " + FIRSTNAME + " " + LASTNAME))" target="DISPLAY_NAME" />
</list>
<converter id="hospisRole" default="unknown">
<entry from="Spitalarzt" to="doctorhospital"/>
<entry from="Arzt" to="doctor"/>
<entry from="Spital / Ärztezentrum / Gemeinschaftspraxis" to="organisation"/>
</converter>
</transform>
<transform>
<!-- Debug -->
<simple target="/FormDefinition/UNNAMED1/Definition/config/pagingLayout/useRestrictivePaging" formula="false"/>
<!-- Start transformation -->
<list sourceList="/FormDefinition/UNNAMED1/Definition/page[list]" addMode="overwrite" condition="sourcePresent">
<list sourceList="./UNNAMED1[list]" addMode="overwrite">
<list sourceList="./group/element[list]" addMode="overwrite">
<simple source="./config/type/@name" target="./@name"/>
<simple source="./config/properties[1]/property/value" target="./config/layoutProps/colSpan"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeName(choosenNode("./config/type")) == "text"" target="./config/type/text/width" default="full"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeName(choosenNode("./config/type")) == "choiceList"" target="./config/type/choiceList/width" default="full"/>
<simple source="./config/type/choiceList/@multiselectTextBox" condition="sourcePresent" target="./config/type/choiceList/@multiselectRepresentation" formula=""textbox""/>
<!-- Individual cases -->
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "ANM_PERS"" target="./config/rules/rule[3]/sourceOnlyScope" default="true"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "WEITERE_BESUCHTE_SCHULEN"" target="./config/type/table/@autoCreateMaxOccurListItems" default="true"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "WEITERE_BESUCHTE_SCHULEN"" target="./config/type/table/@showListControls" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "WEITERE_BESUCHTE_SCHULEN"" target="./config/type/table/@fixedNoOfRows" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_SPRACHKURSE"" target="./config/visibility/expression" formula=""KEINE_SPRACHKURSE != true""/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_SPRACHKURSE"" target="./config/type/table/@autoCreateMaxOccurListItems" default="true"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_SPRACHKURSE"" target="./config/type/table/@showListControls" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_SPRACHKURSE"" target="./config/type/table/@fixedNoOfRows" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_INTEGRATIONSKURSE"" target="./config/visibility/expression" formula=""KEINE_INTEGRATIONSKURSE != true""/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_INTEGRATIONSKURSE"" target="./config/type/table/@autoCreateMaxOccurListItems" default="true"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_INTEGRATIONSKURSE"" target="./config/type/table/@showListControls" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./config/type/@name") == "BESUCHTE_INTEGRATIONSKURSE"" target="./config/type/table/@fixedNoOfRows" default="false"/>
</list>
<simple source="./group/config/@name" target="./group/@name"/>
<simple source="./element/config/type/@name" target="./element/@name"/>
<simple source="./element/config/properties[1]/property/value" target="./element/config/layoutProps/colSpan"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeName(choosenNode("./element/config/type")) == "text"" target="./element/config/type/text/width" default="full"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeName(choosenNode("./element/config/type")) == "choiceList"" target="./element/config/type/choiceList/width" default="full"/>
<simple source="./element/config/type/choiceList/@multiselectTextBox" condition="sourcePresent" target="./element/config/type/choiceList/@multiselectRepresentation" formula=""textbox""/>
<!-- Individual cases -->
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./element/config/type/@name") == "SCHNUPPEREINSAETZE"" target="./element/config/type/table/@autoCreateMaxOccurListItems" default="true"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./element/config/type/@name") == "SCHNUPPEREINSAETZE"" target="./element/config/type/table/@showListControls" default="false"/>
<simple condition="always" enabledFormulaRoot="source" enabled="nodeValue("./element/config/type/@name") == "SCHNUPPEREINSAETZE"" target="./element/config/type/table/@fixedNoOfRows" default="false"/>
</list>
</list>
</transform>
<transform>
<simple source="//orderType" target="/FullSearchOrder/orderType" converter="surveillanceMeasureType" as="orderType"/>
<simple target="/FullSearchOrder/order/type" formula="orderType"/>
<simple enabled="orderType == "ipFull" || orderType == "natFull"" source="//processingDateStart" target="/FullSearchOrder/order/dateTime"/>
<simple enabled="orderType != "ipFull" && orderType != "natFull"" source="//processingDateStart" target="/FullSearchOrder/order/startDate"/>
<simple enabled="orderType != "ipFull" && orderType != "natFull"" source="//processingDateEnd" target="/FullSearchOrder/order/endDate"/>
<simple enabled="EndsWith(nodeValue("//processingDateStart"), "Z")" target="/FullSearchOrder/order/timeZone" default="utc"/>
<simple source="//targetType" target="/FullSearchOrder/order/targetType" as="targetType"/>
<simple source="//targetType" target="/FullSearchOrder/order/type/${orderType}/fieldSelection" />
<simple source="//targetId" target="/FullSearchOrder/order/type/${orderType}/${targetType}" />
<converter id="surveillanceMeasureType" default="">
<from regex=".*_IP" to="ipFull"/>
<from regex=".*_NAT" to="natFull"/>
<from regex=".*_NA" to="naFull"/>
<from regex=".*_TEL" to="telFull"/>
<from regex=".*_EMAIL" to="emailFull"/>
</converter>
</transform>
transformation.xsd