Hidden Gems of Faktor-IPS: User Defined Data Types

In this installment of our hidden gems series, we want to show how easy it is to create your own data types and give you some ideas for data types you might want to use. The concept of data types and which data types Faktor-IPS supports out of the box are described in this PDF(German only for now, sorry). Next to standard Java primitives and value types like int or Double, there are some other predefined data types like Money (a decimal value with fixed precision combined with a currency) and the possibility to use enumerations defined in Faktor-IPS as data types, as well as a small example of defining your very own IsoDate data type.
Let’s look a little closer on that last one.

The data type is an object with some attributes that can be written to and instantiated from a String. To use it, we just add an entry to the <DatatypeDefinitions> section of the .ipsproject file:

As we defined the data type class in our model project, we have to set javaProjectContainsClassesForDynamicDatatypes="true" in the <IpsProject> root tag.

But let’s be honest: the last thing the world of Java needs is another date class. So what else could we do? There are some widely used Java classes from the JRE that can be instantiated from a String. Let’s take the java.util.Currency for example. It is instantiated from an ISO 4217 currency code and returns that code in it’s toString method.

But we can go a little further. Currencies are value objects (every instance with the same currency code is functionally the same). So we can add that property to our datatype(valueObject="true"). We can now use our Currency data type in Faktor-IPS, but we have to know the ISO 4217 currency codes to use it. Wouldn’t it be nice to choose from all known currencies? If you look at the documentation for data type definitions in the .ipsproject file, you can see that there is an isEnumType property with an accompanying getAllValuesMethod. Set the former to true and the latter to getAvailableCurrencies and we are runnig right into a nasty java.lang.ClassCastException: java.util.HashSet cannot be cast to [Ljava.lang.Object;. Unfortunately, Faktor-IPS expects that method to return an array. Let’s not despair, there will be a solution later on, but first, let’s try a class that fits the expectations better, like java.util.TimeZone:

Et voilá, a new IPS data type without a single line of Java code.

Most projects already have a bundle of Enums defined for other systems that they would like to reuse – nothing easier than that. Let’s take our insurance’s line of business enum:

The corresponding data type would be

But using that data type in Faktor-IPS leads to rather ungainly all-caps attribute values. Maybe our Enum already has a more presentable String attribute we can use to display it’s values:

With this, we can tell Faktor-IPS to use the displayName to display the values and only use the enum names in the code:

Now, back to our Currency data type. As we can not edit the class itself, we have two options:

  1. We could create a org.faktorips.datatype.Datatype with an accompanying org.faktorips.codegen.DatatypeHelper that handles custom code generation. More on that later.
  2. We could wrap it in our own class that provides methods with the expected signatures

Let’s look at the second option first.

That lets us finally define our data type as we wanted to before:

But now we have to handle IpsCurrency objects instead of Currency objects in our code and un-/wrap them at our API boundaries. Good enough for an example and hopefully something that can give you new ideas, but in production, we might want to look at option one.

So if we can’t edit the data type class, we can only use it if it’s methods have the right signatures. But what about those classes we can change? Any ideas for data types there? An idea that sometimes comes to mind is that we might want to use our products as a key in a table. So let’s turn a product component type into a data type! To that end, we need to

  1. be able to turn a product component into a String (that’s easy, it already has a getId method)
  2. be able to instantiate it with that id (we might need the help of a runtime repository here)
  3. get all available products so we can choose from the actual products and don’t have to type out runtime ids(again, the runtime repository has a getAllProductComponents method)
  4. and maybe display a nicer name (we could just define a product attribute “DisplayName” and use that)

Points 1 and 4 seem straight forward enough, but how do we get a runtime repository into static methods? A little helper class might be in order. We just need the name of our table of contents file to instantiate a new ClassloaderRuntimeRepository. As we are putting it into a static field but don’t want to reload our workspace every time we add or change a product, we create a manager for the repository that checks whether our table of contents has been updated.

{ private final class ClassloaderRuntimeRepositoryManagerCheckingFileModification extends ClassloaderRuntimeRepositoryManager { private final Path tocResourcePath; private AtomicReference lastModifiedTime; private ClassloaderRuntimeRepositoryManagerCheckingFileModification(ClassLoader classLoader, String basePackage, String pathToToc, Path tocResourcePath) { super(classLoader, basePackage, pathToToc); this.tocResourcePath = tocResourcePath; try { lastModifiedTime = new AtomicReference( Files.getLastModifiedTime(tocResourcePath)); } catch (IOException e) { lastModifiedTime = new AtomicReference(null); } } @Override protected boolean isRepositoryUpToDate(IRuntimeRepository actualRuntimeRepository) { if(actualRuntimeRepository==null)return false; try { FileTime modifiedTime = Files.getLastModifiedTime(tocResourcePath); FileTime lastTime = lastModifiedTime.getAndSet(modifiedTime); return lastTime.equals(modifiedTime); } catch (IOException e) { return false; } } } private final Class

clazz; private ClassloaderRuntimeRepositoryManager repositoryManager; public ProductDatatype(Class

clazz, String tocResource) { this.clazz = clazz; final Path tocResourcePath = Paths.get(tocResource); repositoryManager = new ClassloaderRuntimeRepositoryManagerCheckingFileModification(clazz.getClassLoader(), “”, tocResource, tocResourcePath); } private IRuntimeRepository getRepository() { return repositoryManager.getCurrentRuntimeRepository(); } @SuppressWarnings(“unchecked”) public P byId(String runtimeId) { IProductComponent productComponent = getRepository().getProductComponent(runtimeId); if (clazz.isInstance(productComponent)) { return (P) productComponent; } return null; } public boolean isProductId(String runtimeId) { IProductComponent productComponent = getRepository().getProductComponent(runtimeId); return (clazz.isInstance(productComponent)); } @SuppressWarnings(“unchecked”) public P[] getAllProducts() { List allProductComponents = getRepository().getAllProductComponents(Product.class); return allProductComponents.toArray((P[]) Array.newInstance(clazz, allProductComponents.size())); } }

With this, turning a regular product class into a data type is easy:

And just like that, we can use our product components as a data type, for example as a key in a price table. Of course, this example only works in a combined model and product project; separate projects and deployments might need a little more work, but that is left to the reader as a training exercise, as my professor liked to say.

Finally, let us look at the “real” data types, defined in their own plugin and using custom generated code to see how we can get an even better Currency data type. To do that, we create a new Plugin Project in Eclipse, add a dependency to org.faktorips.devtools.core and create an extension for org.faktorips.devtools.core.datatypeDefinition. We need to provide two classes – the datatype and a helper. The datatype class will extend GenericValueDatatype and implement EnumDatatype. In the constructor, we set a qualified name (what we put into id in the xml), the name of the method to get a value ("getInstance", as before) and the name for the method that checks whether a given value is parseable (to null, as Currency doesn’t offer such a method). We need to implement the methods to get all values (as we did in the wrapper) and to check if a value is parseable(we just try to get a Currency for the given code and return false if that fails). The Helper is just an empty GenericValueDatatypeHelper for our data type.

And now we have Java’s Currency class available as a data type in Faktor-IPS, without having to find and keep up to date a list of all currency symbols ourselves.

Leave a Comment

Your email address will not be published. Required fields are marked *