Scott Finnie
Adventures in oaw, part 1 [1]
OpenArchitectureWare (oaw) is an open source toolkit supporting Model Driven approaches to software development. This is an account of my first ever oaw project, from installation to completed project. The result is a very simple tool; it barely scrapes the surface of oaw's capabilities but is nonetheless usable.
The Project
Getting Things installed
Starting the Project
Expanding
Making it Pretty
Wrapping up
Domain models can provide an effective representation of the concepts and relationships in a problem space. They can be used to generate code directly [2] although are also useful simply as a means to understand the problem at hand. Irrespective of use, their accuracy needs to be validated with stakeholders who know the domain and typically don't know how to read UML models[3]. While it's possible to walk stakeholders through models, reading relationships often causes problems.
In their book "Data Modeling Essentials", Simsion and Witt have a nice way of expressing relationships as textual statements (or business rules).
The rules define templates for rendering relationships in text; here's an example. The following model:
would generate these rules:
Each Dog must be owned by just one Person.
Each Person may own one or more Dogs.
Not every Person has to own a Dog.
Generalising, here's the basic template[4]:
Each Class1 Name {must|may} Relationship Phrase
{just one Class2 Name | one or more Class 2 Plural name}.
{A|An} Class1 Name does not need to Relationship Phrase {a|an} Class2 Name.
The first statement is generated for each direction of each relationship. The second is conditional, only generated when the relationship is optional in a given direction (i.e. lower cardinality bound is 0).
A tool that generated these directly from models would be useful, allowing stakeholders to read the wording as an aid to model validation. Up until now I've neither found nor got round to building anything. But it seemed like a great starter project for my first oaw acquaintance; useful yet not too complex.
The tool would work as follows:
- Read in a uml model
- Generate a set of rule phrases for each relationship
- Output the list to a text file. (Just plain text initially)
oaw provides model parsing out of the box, so the only work required would be creation of the text generation templates.
First off, installation - a simple 3 step process descibed here.
Note: although the instructions recommend using the emf update site I couldn't get it to work. There's a posting on the oaw forums on this, the recommendation being to use the europa download site instead. This worked fine.
Setup done, and pretty painless. Task 1 complete!
Time to get started. I read both the Generating Code from EMF Models and Generating Code from UML2 models tutorials; the latter was closer to my needs, so I downloaded the sample project and imported it into eclipse.
I couldn't get the sample project to build. The generator script complained about illegal characters in the template; when I looked at the template file root.xpt the guillemets (i.e. the French << & >> quotation marks used to demarcate control logic) weren't shown correctly. I suspected some kind of encoding problem and a quick check on the forums confirmed it: the default eclipse workspace encoding doesn't work on a Mac.
Two changes are required:
- Change the default eclipse workspace encoding.
- Add a fileEncoding statement to the generator workflow
This is all covered in a forum posting. The posting also resolves the issue of how to type guillemet characters in the editor. The expand2 reference manual says ctrl-< and ctrl->; the Mac uses
ctrl-\ & ctrl-| instead.
That sorted, the example project built successfully, producing the two java source files as expected in src-gen (Car.java and PersonCar.java).
I did notice that the expand editor was reporting some errors in the Root.xpt template (e.g. 'Couldn't find uml:Model'). However it seemed to build OK so I ignored it.
Finally, time to write some Expand2 template code. As a first, simple, baby step, I added a template clause to handle Method declarations (the example UML2 model included in the project has a method drive() defined on class Car). No problems here; template clause added and the method signature duly appeared in Car.java. A small step perhaps, but it's always rewarding and encouraging when something just works. Here's the template:
«DEFINE Root FOR uml::Model»
«EXPAND PackageRoot FOREACH (List[uml::Package])ownedElement»
«ENDDEFINE»
«DEFINE PackageRoot FOR uml::Package»
«EXPAND ClassRoot FOREACH (List[uml::Class])ownedType»
«ENDDEFINE»
«DEFINE ClassRoot FOR uml::Class»
«FILE name+".java"»
public class «name» {
«EXPAND MethodRoot FOREACH (List[uml::Operation])ownedOperation»
}
«ENDFILE»
«ENDDEFINE»
«DEFINE MethodRoot FOR uml::Operation»
void «name» ();
«ENDDEFINE»
and here's the resulting Car.java:
public class Car {
void drive();
}
Association Templates at last
I updated the sample uml2 model to include a simple, bi-directional association. I also added some attributes, a primitive type and an enum. Running the workflow, I got an error:
1942 ERROR - No Definition 'PackageRoot for uml::PrimitiveType' found!
This is a result of Xpand's polymorphic lookup. It found the primitive type in the list of the Package's members but couldn't find a matching <<DEFINE>> clause for it. I only wanted to deal with associations, so needed to be more precise about the quantifiers.
Here's how the template looked after editing:
«DEFINE Root FOR uml::Model»
«EXPAND PackageRoot FOREACH (List[uml::Package])ownedElement»
«ENDDEFINE»
«DEFINE PackageRoot FOR uml::Package»
«FILE name+".txt"»
«EXPAND AsssociationRoot FOREACH (List[uml::Association])ownedAssoc»
«ENDFILE»
«ENDDEFINE»
«DEFINE AssociationRoot FOR uml::Association»
Association: «name»
«ENDDEFINE»
(Mis)Interpreting Quantifiers
I tried running this, but got the same error as before: no definition for uml:PrimitiveType. Hmm. Didn't expect that. I interpreted the second «DEFINE» clause as 'Apply the AssociationRoot template for each instance of uml:Association in the Package". I assumed it would simply ignore any other package children of a different kind (i.e. non-uml:Association).
Having tried the xpand2 and expressions reference manuals without success it was off to the forums. The response was very quick & helpful: I'd misinterpreted the quantifier expression syntax. The list type expression doesn't affect the list elements selected; to narrow the selection requires the use of typeSelect. A fairly simple change that sorted the problem: this
«EXPAND PackageRoot FOREACH (List[uml::Package])ownedElement»
became
«EXPAND PackageRoot FOREACH packagedElement.typeSelect(uml::Package)»
Having found out how to locate associations, I went about finding their properties. I discovered at this point the EMF editor doesn't always put complete details into models, notably upper/lower bounds on cardinality. I decided at that point to look for a graphical modelling tool. MagicDraw is used within the oaw project for some testing so I went with that.
I installed MagicDraw 12.5, built a model and exported it as EMF UML2v2 XMI. The xmi is slightly different in format to that produced by the native EMF editor (notably in how it records relationships) but was easy to navigate. I created a new model and updated the workflow definition.
Here's the second version of the template:
«DEFINE Root FOR uml::Model»
«FILE name+".txt"»
«EXPAND PackageRoot FOREACH ownedElement.typeSelect(uml::Package)»
«ENDFILE»
«ENDDEFINE»
«DEFINE PackageRoot FOR uml::Package»
Package: «getQualifiedName()»
«EXPAND AssociationRoot FOREACH packagedElement.typeSelect(uml::Association)»
«EXPAND PackageRoot FOREACH packagedElement.typeSelect(uml::Package)»
«ENDDEFINE»
«DEFINE AssociationRoot FOR uml::Association»
Association: «name»
«FOREACH memberEnd AS me»
«me.class.name» «me.name» [«me.lowerValue.value»..«me.upperValue.value»] «me.type.name»
«ENDFOREACH»
«ENDDEFINE»
When run on the sample dog - owner model above it produced this result:
Package: DogsDomainModel::Ownership Subsystem
Association: R1
Dog be owned by [1..1] Person
Person own [0..-1] Dog Clearly still some work to do on formatting, but the basic information is now there - in 16 lines of template!
A quick recap of what's still remaining:
- Add in
may or must based on lower bound value (may if 0, must otherwise).
- Similarly substitute the upper bound (
just one or one or more)
- Add in the extra clause for optionality
- Work out how to pluralise the object of the sentence for the many case;
- Ensure appropriate use of 'a' or 'an' according to the noun in the optionality clause.
I couldn't see an easy way to translate the upper & lower bounds directly in xpand; while possible with 'if' clauses, it got very messy. In many ways that's a good thing. The template should be focused on outputting text - not deriving properties from the model. Oaw provides the xtend language that's more appropriate for this type of problem. Here's the contents of the file (named Association.ext):
import uml;
String optionality(Integer lowerbound):
lowerbound == 0 ? "may" : "must";
String cardinality(Integer upperbound):
upperbound == 1 ? "just one" : "one or more";
String pluralise(String noun, Integer upperbound):
upperbound == 1 ? noun : noun+"s";
and here's the updated template (which also includes the optionality clause):
«EXTENSION Association»
«DEFINE Root FOR uml::Model-»
«FILE name+".txt"-»
Model: «name»
«EXPAND AssociationRoot FOREACH ownedElement.typeSelect(uml::Association)-»
«EXPAND PackageRoot FOREACH ownedElement.typeSelect(uml::Package)-»
«ENDFILE»
«ENDDEFINE»
«DEFINE PackageRoot FOR uml::Package»
Package: «getQualifiedName()»
«EXPAND AssociationRoot FOREACH packagedElement.typeSelect(uml::Association)-»
«EXPAND PackageRoot FOREACH packagedElement.typeSelect(uml::Package)-»
«ENDDEFINE»
«DEFINE AssociationRoot FOR uml::Association»
«FOREACH memberEnd AS me-»
Each «me.class.name» «optionality(me.lowerValue.value)» «me.name» «cardinality(me.upperValue.value)»
«pluralise(me.type.name, me.upperValue.value)-».
«IF me.lowerValue.value == 0-»
Not every «me.class.name» has to «me.name» a «me.type.name-».
«ENDIF-»
«ENDFOREACH-»
«ENDDEFINE» Running on the sample model produces the following:
Model: DogsDomainModel
Package: DogsDomainModel::Ownership Subsystem
Each Dog must be owned by just one Person.
Each Person may own one or more Dogs.
Not every Person has to own a Dog.
Package: DogsDomainModel::UML Standard Profile
The result is now pretty close. Some other points to note in this version:
- The
Association.ext file is imported in the EXTENSION statement on the first line of the template;
- The proliferation of '-' characters at the end of the control statements. These prevent excess empty lines in the generated file.
Adding some Java into the Mix
Almost there now. It just remained to ensure appropriate use of {'a' | 'an'}. That requires some string manipulation which (as far as I could see) was beyond the facilities provided natively in extend. However, extend caters for this by enabling calls to java class methods from within extend functions. Here's the relevant section from Association.ext:
String indefiniteArticle(String noun, Boolean capitalise):
JAVA org.domainmodels.reltext.AssociationExtension.indefiniteArticle(java.lang.String,
java.lang.Boolean);
and here's the java class it calls (AssociationExtension.java):
package org.domainmodels.reltext;
public class AssociationExtension {
public final static String indefiniteArticle(String noun, Boolean capitalise) {
String regex = new String("a|e|i|o|u");
String ia = new String("a");
if (noun.substring(0,1).toLowerCase().matches(regex) |
noun.toLowerCase().equals("hotel")) {
ia = "an";
}
if (capitalise) {
return (capitalise(ia));
} else {
return (ia);
}
}
public static final String capitalise(String word) {
return (word.substring(0, 1).toUpperCase() + word.substring(1));
}
}
While the java won't win any elegance awards it does the trick. And here's the final version of the template:
«EXTENSION Association»
«DEFINE Root FOR uml::Model-»
«FILE name+".txt"-»
Model: «name»
«EXPAND AssociationRoot FOREACH ownedElement.typeSelect(uml::Association)-»
«EXPAND PackageRoot FOREACH ownedElement.typeSelect(uml::Package)-»
«ENDFILE»
«ENDDEFINE»
«DEFINE PackageRoot FOR uml::Package»
Package: «getQualifiedName()»
«EXPAND AssociationRoot FOREACH packagedElement.typeSelect(uml::Association)-»
«EXPAND PackageRoot FOREACH packagedElement.typeSelect(uml::Package)-»
«ENDDEFINE»
«DEFINE AssociationRoot FOR uml::Association»
«FOREACH memberEnd AS me-»
«REM»Each <subject> {must|may} <relationship> {exactly one | many} <object>[s] «ENDREM»
Each «me.class.name» «optionality(me.lowerValue.value)» «me.name» «cardinality(me.upperValue.value)»
«pluralise(me.type.name, me.upperValue.value)-».
«IF me.lowerValue.value == 0-»
«REM»A[n] <subject> need not <relationship> a[an] <object>«ENDREM»
«indefiniteArticle(me.class.name, true)» «me.class.name» need not «me.name»
«indefiniteArticle(me.class.name, false)» «me.type.name-».
«ENDIF-»
«ENDFOREACH-»
«ENDDEFINE»
That's it. Here's the final output for the test model (note I changed the phrasing of the optional clause a little from Simsion & Witt's standard because I think it reads slightly better):
Model: DogsDomainModel
Package: DogsDomainModel::Ownership Subsystem
Each Dog must be owned by just one Person.
Each Person may own one or more Dogs.
A Person need not own a Dog.
So that's it; end of my first foray into oaw. A few very minor hiccups along the way, but by and large a very easy, painless and productive experience. Three source files and around 50 lines of code to achieve the objective. That's really quite impressive, testament to oaw's capabilities.
As for downsides, really nothing significant. I wasn't able to find an answer to my type quantifier question from the reference docs, but the forum response was
excellent. Ditto for some queries about calling java extensions from extend and migrating the project from v4.1.2 to 4.2.
So first installment complete. There's plenty features that could be added, but that's for another day. For the meantime, thanks to the oaw team for a great toolkit that finally got me a solution that I'm already using in my day job.
Finally, I'd be interesting in hearing any comments or suggestions you might have (you'll need to delete the crude anti-spam measures from the email address). Thanks.