Die verborgene Schätze von Faktor-IPS: Eigene Datentypen definieren

In dieser Folge der verborgenen Schätze von Faktor-IPS möchte ich zeigen wie einfach es ist eigene Datentypen zu erstellen und einige Anregungen geben, welche Datentypen man verwenden könnte. Das Konzept der Datentypen und welche von Faktor-IPS direkt unterstützt werden eklärt dieses PDF. Neben Standard-Java-Primitiven und Wertetypen wie int und Double gibt es ein paar vordefinierte Datentypen wie Money (ein dezimaler Betrag mit fester Genauigkeit, kombiniert mit einer Währung) und die Möglichkeit in Faktor-IPS definierte Aufzählungen als Datentypen zu verwenden. Im PDF gibt es auch ein kleines Beispiel für einen eigenen IsoDate
Datentypen, das ich hier etwas näher betrachten möchte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | publicclassIsoDate{ privateintyear; privateintmonth; privateintday; publicIsoDate(intyear,intmonth,intday){ this.year=year; this.month=month; this.day=day; } publicintgetYear(){ returnyear; } publicintgetMonth(){ returnmonth; } publicintgetDay(){ returnday; } publicStringtoString(){ Stringm=month<10?„0“+month:„“+month; Stringd=day<10?„0“+day:„“+day; returnyear+‚-‚+m+‚-‚+d; } @Override publicinthashCode(){ // TODO } @Override publicbooleanequals(Objectobj){ // TODO } publicstaticfinalIsoDate valueOf(Strings){ if(s==null||s.equals(„“)){ returnnull; } String[]tokens=s.split(„-„); intyear=Integer.parseInt(tokens[0]); intmonth=Integer.parseInt(tokens[1]); intdate=Integer.parseInt(tokens[2]); returnnewIsoDate(year,month,date); } } |
Der Datentyp ist ein Objekt mit ein paar Attributen, das in einen String geschrieben und aus ihm instatiiert werden kann. Um den Datentypen zu verwenden muss der <DatatypeDefinitions>
-Section der .ipsproject-Datei ein Eintrag hinzugefügt werden:
1 | <Datatype id=„IsoDate“javaClass=„org.faktorips.sample.datatypes.IsoDate“valueOfMethod=„valueOf“ /> |
Da der Datentyp direkt im Modellprojekt definiert ist muss noch javaProjectContainsClassesForDynamicDatatypes="true"
im Wurzeltag <IpsProject>
gesetzt werden.
Aber um ehrlich zu sein ist noch eine Datumsklasse so ziemlich das letzte was die Java-Welt braucht. Welche anderen Beispiele können wir finden? Es gibt einige weithin genutzte Klassen im JRE, die aus einem String instantiiert werden können. Nehmen wir zum Beispiel java.util.Currency
. Ein Währungsobjekt wird aus einem ISO 4217 Währungscode erstellt der auch von toString
zurückgegeben wird.
1 | <Datatype id=„Währung“javaClass=„java.util.Currency“valueOfMethod=„getInstance“ /> |
Wir können noch einen Schritt weiter gehen. Währungen sind Wertobjekte (jede Instanz mit dem gleichen Währungscode ist funktional gleich). Dies können wir unserem Datentypen als Attribut hinzufügen(valueObject="true"
). Jetzt können wir die Währung in Faktor-IPS verwenden, aber wir müssen die ISO 4217 Währungscodes kennen und als String eintippen um sie zu benutzen. Wäre es nicht besser, aus einer Liste aller bekannter Währungen auswählen zu können? Wenn man die Dokumentation der DatatypeDefinitions in der .ipsproject
-Datei betrachtet findet man eine Eigenschaft isEnumType
mit zugehöriger getAllValuesMethod
. Erstere auf true
gesetzt un zweitere auf getAvailableCurrencies
und schon laufen wir in eine unschöne java.lang.ClassCastException: java.util.HashSet cannot be cast to [Ljava.lang.Object;
. Leider erwartet Faktor-IPS ein Array als Rückgabewert. Keine Sorge, weiter unten gibt es auch für dieses Problem eine Lösung, aber zuerst möchte ich eine andere Klasse betrachten, die besser auf die Erwartungen passt; java.util.TimeZone
zum Beispiel:
1 2 3 4 5 6 | <Datatype id=„TimeZone“ javaClass=„java.util.TimeZone“ valueOfMethod=„getTimeZone“ valueObject=„true“ isEnumType=„true“ getAllValuesMethod=„getAvailableIDs“/> |
Et voilá, ein neuer IPS-Datentyp ohne eine einzige Zeile Javacode.
Die meisten Projekte haben bereits einige Aufzählungen für andere Systeme definiert, die sie gerne auch mit Faktor-IPs weiter verwenden möchten – nichts leichter als das. Nehmen wir als Beispiel die Sparten einer Versicherung:
1 2 3 4 5 6 | publicenumLineOfBusiness{ FIRE, LIFE, HEALTH, LIABILITY } |
Der zugehörige Datentyp sähe zum Beispiel so aus:
1 2 3 4 5 6 | <Datatype id=„Sparte“ javaClass=„org.faktorips.sample.datatypes.LineOfBusiness“ valueObject=„true“ isEnumType=„true“ valueOfMethod=„valueOf“ getAllValuesMethod=„values“ /> |
Aber wenn man diesen Datentypen in Faktor-IPS nutzt sieht das mit den komplett groß geschriebenen Enum-Literalen nicht all zu gut aus. Vielleicht hat unsere Aufzählung ja auch schon ein nutzerfreundlicheres String-Attribut das wir verwenden können:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | publicenumLineOfBusiness{ FIRE(„Feuer“), LIFE(„Leben“), HEALTH(„Kranken“), LIABILITY(„Haftpflicht“); privatefinalStringdisplayName; privateLineOfBusiness(StringdisplayName){ this.displayName=displayName; } publicStringgetDisplayName(){ returndisplayName; } } |
Damit können wir Faktor-IPS anweisen den displayName
zur Anzeige zu verwenden und die Enum-Literale lediglich im Code:
1 2 3 4 5 6 7 8 | <Datatype id=„Sparte“ javaClass=„org.faktorips.sample.datatypes.LineOfBusiness“ valueObject=„true“ isEnumType=„true“ valueOfMethod=„valueOf“ getAllValuesMethod=„values“ isSupportingNames=„true“ getNameMethod=„getDisplayName“ /> |
Damit zurück zum Währungsdatentyp. Da wir die Klasse Currency
nicht verändern können haben wir zwei Möglichkeiten:
- Wir können einen
org.faktorips.datatype.Datatype
nebst zugehörigemorg.faktorips.codegen.DatatypeHelper
erstellen, die sich um die Erzeugung des passenden Zugriffscodes kümmern. Dazu später mehr. - Wir können sie in unsere eigene Klasse verpacken, deren Methoden die erwartete Signatur bedienen.
Zuerst die zweite Option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | publicclassWaehrung{ privatefinalCurrency currency; privateWaehrung(Currency currency){ this.currency=currency; } publicCurrency getCurrency(){ returncurrency; } publicinthashCode(){ returncurrency.hashCode(); } publicbooleanequals(Objectobj){ returnobj instanceofWaehrung&¤cy.equals(((Waehrung)obj).currency); } publicStringtoString(){ returncurrency.toString(); } publicstaticWaehrung getInstance(StringwaehrungsCode){ returnnewWaehrung(Currency.getInstance(waehrungsCode)); } publicstaticWaehrung[]getVerfuegbareWaehrungen(){ returnCurrency.getAvailableCurrencies().stream() .map(Waehrung::new) .collect(Collectors.toList()) .toArray(newWaehrung[0]); } } |
Damit können wir den Datentypen so definieren:
1 2 3 4 5 6 | <Datatype id=„Currency“ javaClass=„org.faktorips.sample.datatypes.Waehrung“ valueOfMethod=„getInstance“ valueObject=„true“ isEnumType=„true“ getAllValuesMethod=„getVerfuegbareWaehrungen“ /> |
Nur müssen wir jetzt immer mit Waehrungs- statt mit Currency-Objekten arbeiten und diese an Schnittstellen ein- und auspacken. Gut genug für ein Beispiel und hoffentlich etwas das zu eigenen Datentypen anregt, aber vielleicht ist hier doch Variante 1 die richtige.
Wenn wir die Datentyp-Klasse nicht verändern können müssen wir hoffen, dass ihre Signaturen stimmen. Aber was ist mit unseren eigenen Klassen, die wir anpassen können? Welche Klassen fallen einem da ein? Ein Beispiel könnten unsere Faktor-IPS-Produktbausteine sein. So mancher wünschte sich schon, diese direkt als Schlüssel in einer Tabelle zu verwenden. Also verwandeln wir eine Produktbaustein-Klasse in einen Datentypen. Dazu brauchen wir
- eine Möglichkeit einen Produktbaustein in einen String zu verwandeln (einfach; es gibt bereits die Methode
getId
) - eine Möglichkeit ihn aus diesem ID-String zu erzeugen (wozu wir wohl ein RuntimeRepository brauchen)
- die Liste aller verfügbaren Produktbausteine, um diese direkt auswählen zu können statt fehleranfällig und unvalidiert Runtime-IDs einzutippen (auch hier sollte das RuntimeRepository mit seiner Methode
getAllProductComponents
hilfreich sein) - möglicherweise einen besser zur Anzeige geeigneten Namen (den wir einfach mit einem eigenen Produktattribut definieren können)
Die Punkte 1 und 4 sind offensichtlich leicht zu erfüllen, aber wie kriegen wir ein RuntimeRepository in unsere statischen Methoden? Eine kleine Helferklasse scheint angebracht. Mit dem Namen der Table-of-Contents-Datei können wir ein neues ClassloaderRuntimeRepository
erzeugen. Da wir es in einem statischen Kontext halten und nicht jedes Mal wenn wir einen neuen Produktbaustein anlegen oder ändern unseren Arbeitsbereich neu laden möchten erstellen wir einen Manager für das Repository der prüft, ob der ToC aktualisiert wurde.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | publicclassProductDatatype<PextendsProductComponent>{ privatefinalclassClassloaderRuntimeRepositoryManagerCheckingFileModificationextendsClassloaderRuntimeRepositoryManager{ privatefinalPath tocResourcePath; privateAtomicReference<FileTime>lastModifiedTime; privateClassloaderRuntimeRepositoryManagerCheckingFileModification(ClassLoader classLoader,StringbasePackage, StringpathToToc,Path tocResourcePath){ super(classLoader,basePackage,pathToToc); this.tocResourcePath=tocResourcePath; try{ lastModifiedTime=newAtomicReference<FileTime>( Files.getLastModifiedTime(tocResourcePath)); }catch(IOExceptione){ lastModifiedTime=newAtomicReference<FileTime>(null); } } @Override protectedbooleanisRepositoryUpToDate(IRuntimeRepository actualRuntimeRepository){ if(actualRuntimeRepository==null)returnfalse; try{ FileTime modifiedTime=Files.getLastModifiedTime(tocResourcePath); FileTime lastTime=lastModifiedTime.getAndSet(modifiedTime); returnlastTime.equals(modifiedTime); }catch(IOExceptione){ returnfalse; } } } privatefinalClass<P>clazz; privateClassloaderRuntimeRepositoryManager repositoryManager; publicProductDatatype(Class<P>clazz,StringtocResource){ this.clazz=clazz; finalPath tocResourcePath=Paths.get(tocResource); repositoryManager=newClassloaderRuntimeRepositoryManagerCheckingFileModification(clazz.getClassLoader(),„“,tocResource,tocResourcePath); } privateIRuntimeRepository getRepository(){ returnrepositoryManager.getCurrentRuntimeRepository(); } @SuppressWarnings(„unchecked“) publicPbyId(StringruntimeId){ IProductComponent productComponent=getRepository().getProductComponent(runtimeId); if(clazz.isInstance(productComponent)){ return(P)productComponent; } returnnull; } publicbooleanisProductId(StringruntimeId){ IProductComponent productComponent=getRepository().getProductComponent(runtimeId); return(clazz.isInstance(productComponent)); } @SuppressWarnings(„unchecked“) publicP[]getAllProducts(){ List<Product>allProductComponents=getRepository().getAllProductComponents(Product.class); returnallProductComponents.toArray((P[])Array.newInstance(clazz,allProductComponents.size())); } } |
Damit ist es einfach, eine normale Produktbausteinklasse zum Datentyp zu machen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | privatestaticfinalProductDatatype<Produkt>DT=newProductDatatype<>(Produkt.class, „org/faktorips/sample/model/internal/faktorips-repository-toc.xml“); publicstaticProdukt byId(StringruntimeId){ returnDT.byId(runtimeId); } publicstaticbooleanisProductId(StringruntimeId){ returnDT.isProductId(runtimeId); } publicstaticProdukt[]getAlleProdukte(){ returnDT.getAllProducts(); } |
1 2 3 4 5 6 7 8 9 10 | <Datatype id=„Produkt“ javaClass=„org.faktorips.sample.model.Produkt“ valueObject=„true“ isEnumType=„true“ valueOfMethod=„byId“ isParsableMethod=„isProductId“ valueToStringMethod=„getId“ getAllValuesMethod=„getAlleProdukte“ isSupportingNames=„true“ getNameMethod=„getAnzeigeName“ /> |
Und schon können wir unsere Produktbausteine als Datentypen verwenden, zum Beispiel als Schlüssel in einer Preistabelle. Natürlich funktioniert dieses Beispiel so nur in einem kombinierten Modell- und Produktprojekt; um mit getrennten Projekten und Deployments klar zu kommen brucht es noch etwas Arbeit, aber das sei dem Leser zur Übung gelassen, wie mein Professor zu sagen pflegte.
Zum Schluss möchte ich noch auf die „echten“ Datentypen eingehen, die in einem eigenen Plugin definiert werden und für die angepasster Code generiert wird, um einen möglicherweise noch besseren Währungs-Datentyp zu erhalten. Als erstes muss in Eclipse ein Plugin-Projekt angelegt werden, in dem eine Abhängigkeit zu org.faktorips.devtools.core
und eine Erweiterung zu org.faktorips.devtools.core.datatypeDefinition
angelegt werden. Dazu benötigen wir zwei Klassen: Den Datentyp und einen zugehörigen Helper. Der Datentyp leitet von GenericValueDatatype
ab und implementiert EnumDatatype
. Im Konstruktor setzen wir den qualifizierten Namen (wie wir ihn im XML bei id
eingetragen hatten), den Namen der Methode zur Erzeugung eines Wertobjekts("getInstance"
, wie zuvor) und den Namen der Methode die prüft, ob ein gegebener Wert geparst werden kann (auf null
, da Currency keine solche Methode hat). Wir müssen Methoden implementieren, die alle Werte zurückgeben (wie in unserer Wrapper-Klasse) und die parsbarkeit prüfen(dazu versuchen wir einfach eine Currency anzulegen und geben im Fehlerfall false
zurück). Der Helper ist nur ein leerer GenericValueDatatypeHelper
für unseren Datentyp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | publicclassWaehrungDatatypeextendsGenericValueDatatypeimplementsEnumDatatype{ publicWaehrungDatatype(){ setQualifiedName(„Waehrung“); setValueOfMethodName(„getInstance“); setIsParsableMethodName(null); } @Override publicString[]getAllValueIds(booleanincludeNull){ returnCurrency.getAvailableCurrencies().stream().map(Currency::toString).collect(Collectors.toList()) .toArray(newString[0]); } @Override publicbooleanisParsable(StringcurrencyCode){ try{ returnCurrency.getInstance(currencyCode)!=null; }catch(Exceptione){ returnfalse; } } @Override publicbooleanisSupportingNames(){ returnfalse; } @Override publicStringgetValueName(Stringid){ thrownewUnsupportedOperationException(getClass().getName()+“ is not supporting names.“); } @Override publicClass<?>getAdaptedClass(){ returnCurrency.class; } @Override publicStringgetAdaptedClassName(){ returnCurrency.class.getName(); } } |
1 2 3 4 5 6 7 | publicclassWaehrungDatatypeHelperextendsGenericValueDatatypeHelper{ publicWaehrungDatatypeHelper(){ super(newWaehrungDatatype()); } } |
Jetzt haben wir Javas Currency-Klasse als Faktor-IPS-Datentyp und müssen nicht selbst eine Liste mit allen Währungen pflegen.
Update 22.6:
Faktor-IPS 22.6 bringt einige Verbesserungen für selbstdefinierte Datentypen:
- Die Methode, die alle Werte zurückgibt, kann nun statt einem Array auch eine Collection als Rückgabewert haben.
- Auch Nicht-Aufzählungs-Datentypen können neben einer ID einen Namen haben.
- Currency ist ein offiziell in Faktor-IPS unterstützter Datentyp