Ecore models are used for modelling data and among others generating Java code for representing the data. The model includes information about classes and attributes, inheritance and associations, which in effect defines and limits what structures of objects that are valid for the modelled domain. E.g. a model of the domain of libraries and books may express that all books have a title and an author and is owned by some library. A library may lend books to people (persons) and record the return date. EMF will make sure that all instance data will be correct according to these structural rules. However, since there are limits to what can be directly expressed in Ecore, you can still build invalid data, e.g. give a person a name with illegal characters. The solution is to go outside Ecore and implement data rules in Java and hook it into EMF's validation mechanism. The downside is that these rules are not part of the model and relies on code generation and compilation.
Invariants, constraints, derived features and operation bodies
So what kind of annotations are provided and what are they used for? There are four kinds:
- Constraints are boolean expressions that are attached to a class (EClass) and will tell if an instance is invalid. These are used for simple checks.
- Invariants are similar to constraints, but are implemented by methods (EOperations) with a certain signature (return value and parameter list). These are used when you want more control over the validation process.
- Derived features are features that compute their values from other features. These are used when you want to access values in the same manner, independent of what values are actually stored or computed.
- Operation bodies are the actual implementation of procedural logic, that you would otherwise write in Java.
Let's look at an example and see how js4emf supports them.
We'll take the following Ecore model as the starting point:
We'll start by considering the name attribute of the Person class. A name should be a sequence of letters and spaces, and to express this as a constraint we do the following:
- Select the Person class and open the view EMF Delegates View
- Select the Add constraint button in the view's toolbar
- Give the constraint a name, e.g. nameHasLetterAndSpaceOnly
Invariant for borrowed books
An alternative to constraints is using invariants. An invariant is similar to a constraint, but is explicitly modelled as a method with a specific signature. This gives better control over how the invariant is used in the validation process. We'll implement the rule that all the books associated with a loan must be have the same owning library as the loan object.
- Select the Loan class (and open the EMF Delegates view).
- Select the add invariant in the view's toolbar.
- Provide the name of the operation implementing the invariant.
This will create the operation with the appropriate signature (right figure below) and if you may select it and enter the operation's body (left figure below). In this case we check that the library owning this loan (this.eContainer) also owns the borrowed book.
Derived features for given and family names
The next enrichment will be the derived features givenName and familyName. The givenName is supposed to be the first word of the name attribute and the familyName the last, with the familyName taking priority if the name only contains one word. To add the appropriate code, do the following:
- Select the givenName (or familyName) feature.
Operation body for getLoansDueBefore
The final enrichment of the model is providing the implementation of the method (EOperation) getLoansDueBefore in the Library class. This method should take a Date (EDate) argument and return the loans that are due before this date. The steps are the following:
- Select the getLoansDueBefore method and open the EMF Delegates view
- Enter the body of the method (see left figure below)
Again, this will add the appropriate annotations, as shown in the right figure below.
Testing the constraints, invariants, derived features and operation bodies
To test that our enrichments/annotations work, we need some instance data. This can easily be created using a standard feature of EMF. Right-click on the Library Model class and select Create Dynamic Instance... This will create a new file (by default named LibraryModel.xmi) containing a (serialized) Library Model instance. This instance will be the root container of the Library and Person objects in our model instance. The file should open in the Sample Reflective Ecore Model Editor, if not reopen it with this editor. You may now enter sample instance data by creating children of the Library Model instance. To test our annotations, we at least need a library with a couple of books, a person and some loan objects owned by the library and borrowed by the person. A minimal example is shown below. This model instance contains one library with one book by Dostojevskij that Hallvard has borrowed and must return by 17th of May (incidently, Norway's national day).
To try invariants and constraints we select the Validate action in the Sample Reflective Editor menu. To check the derived name features we can open the Properties view on Dostojevskij or Hallvard Person instances, e.g. right-click and select Show Properties View. Finally, to test the getLoansDueBefore(EDate) operation, open the EMF Invoke EOperation view.