Developer documentation
If you want to find out how to extend the Generator and adapt it to fit your own needs, this page is for you.
General architecture and workflow
When invoking the Generator, it will (roughly) perform the following actions:
Each of these steps has a corresponding Scala class (called Services hereafter) to take care of it. So, when extending the Generator there are two basic approaches:
-
modifying one of the Services (either directly or by means of subclassing)
-
intercepting the workflow by adding more Services
To give you a rough idea of what Services are available and where they will be located, here is an overview of the Generator’s architecture:
as the CLI part of the Generator just means parsing the parameters and setting up the configuration data for the Generator accordingly, these steps are not discussed here any further. Consult the corresponding Scaladoc and source code for details. |
The Scala Abstract class representation
The Abstract class representation (ACR) is the hearth of the Generator. It forms the meta-model for the code to generate and provides facilities to adapt model instances for different purposes.
In short each element of a Scala class has a matching class in the ACR. E.g. a method will be
represented by an instance of SMethod
, attributes become SAttribute
s and a class itself will
be mapped to a SClass
. As the main purpose of the Generator is to provide executable software
models, an abstraction of a model, the SModel
is provided as well. It is pretty straightforward
and simply provides all the classes in the model.
Types and Classes
Models seldom start from scratch. Instead, they will reuse certain data types, such as String
s,
Integer
s, etc. as attributes in the classes that are specified by the model. Usually these data
types are provided by some library and may be mapped to a basic data types of the target programming
language (or a converter is provided along with them).
The Generator follows a similar approach: it provides an abstraction for user-supplied classes (i.e.
the classes specified within the model). This is the SClass
class already mentioned above.
Additionally a SType
class is provided to handle predefined data types. When generating source
code these types are expected to exist "as are" and will not be tackled further. I.e. no .scala
files will be created for SType
instances.
As types and classes are concepts only found at a more abstract level and are irrelevant when it
comes to Scala source code, they share a common superclass - STypedElement
- which will be used
whenever type information (no matter if it is user-supplied or generic) is needed.
Working with the ACR on a more abstract level
The ACR forms a trade-off between an abstract meta-model on one side, and a straightforward (hence
simplified) usage on the other side. Therefore low-level abstraction will oftentimes be wrapped by
some higher abstraction. E.g. a SMethod
consists of abstract SMethodStatement
s and may thus
provide abstract functionality for modification. However a SMethodStatement
simply wraps a
String
(the actual statement). Dealing with the statements themselves may therefore become
pretty cumbersome at a certain point. To circumvent this issue, the ACR's elements should be
extended by means of subclassing to provide more tailored versions which handle the additional
complexity.
A good example for this approach is the extension of the SMethod
class to easily generate
Getters and Setters for attributes:
class SMethod(val name: String,
val result: STypedElement,
val params: Seq[SMethodParameter],
var implementation: Seq[SMethodStatement]) {
// implementation omitted
}
class SGetter(attr: SAttribute) extends SMethod(
name = s"get${attr.name.firstLetterToUpperCase}",
result = attr.attrType,
params = Seq.empty,
implementation = Seq(SMethodStatement(content = attr.name, usedTypes = Set(attr.attrType))) {
// implementation omitted
}
Getters may now be generated by only specifying the attribute they are created for - the cumbersome creation of the actual method statements is hidden within the class implementation.
Working with models
The Generator's goal is to transform a synchronization model into a JAR file which may than
be loaded, displayed and modified by the Synchronization UI. In order to do so, the Generator
creates a SModel
which is a simple abstraction of such a model. It contains all classes as
instances of the ACR.
Incremental construction of SModel
instances
Adding new classes to an SModel
instance is pretty straightforward: its default implementation
(SimpleSModel
) already provides an addModelClass(clazz)
method.
However one often encouters the issue of some kind of cyclic reference in a model, i.e. a class C0
referencing other classes C1, … Cn
which in turn keep references until one of these references
C0
again. In this case only calling SimpleSModel.addModelClass(C0)
will not work as it requires
C1, … Cn
to already be created. This leads to C0
requiring itself to be created already.
Thus a SClass
does not need to be created in one go but can be extended later on. To keep track
of all classes which are already (at least to some extend) build, the STypeRegistry
was created.
It will work as a repository for all SClass
es
So instead of creating a SClass
from scratch each time it is referenced, the STypeRegistry
should be queried. Only if the query does not yield any results, a new class has to be constructed.
SClass
only if requiredval clazz: SClass = STypeRegistry
.query(clazzName, clazzPackage)
.getOrElse {
val newClazz: SClass = new SClass(name = clazzName, sPackage = clazzPackage)
STypeRegistry.addType(newClazz)
newClazz
}
The STypeRegistry
already provides instances for some default data types so they do not need to be
created.
Building a model from EMF
Currently the generator only supports generating models from EMF. When dealing with an ecore some additional problems have to be faced. The most prominent one is that EMF does not use the basic data types of Java and Scala directly but wraps them with an EMF specific class instead.
When resolving such a type without further attention, it will simply be converted to an SType
.
Hence compilation (see Writing to the File system) will fail as the required types are not present and they do
not work as expected (e.g. the +
operator may not be applied to an EString
).
To solve this issue, the EMF types need to be mapped to their Java-equivalents. This is what the
EmfTypeTranslator
should be used for. Its getSClassFromEmf
method will provide the matching
types for all EMF wrappers and nothing for all other classes.
Modifying SModel
instances
To change the contents of a SModel
, the whole ACR implements the
Visitor pattern[1].
This approach is used by the whole Generator workflow.
More precisely the following Visitors are predefined:
-
GetterSetterGeneratingVisitor
will generate the necessary methods for all attributes -
SyncEnhancingVisitor
will make the setters notify the Synchronization context when an attribute is updated and insert the model’s classes into the Synchronization’s inheritance hierarchy -
SModelFSWriter
will write the model’s classes to the file systems, compile them and generate a JAR file for the whole model
Preparing a model for synchronization
When creating JVM bytecode for EMF models, the Generator cannot just convert the model to Scala code and compile it. Instead, it has to make various preparations as the resulting (compiled) Scala code has to meet a number of requirements. These are necessary to make the code runnable in a Synchronization context and work as expected, i.e. synchronize its state with a number of other models.
These requirements are:
-
each synchronized instance has to extend the
PlayerSync
class -
when any constructor is invoked, it has to call the
buildClass
method. This method takes care of notifying the Synchronization context and setting up the necessary roles. It is provided by thePlayerSync
class -
every time a Setter is called, it has to notify the Synchronization context about the change performed
An EMF model will usually not meet any of them as it was generated without synchronization in mind. Furthermore some difficulties are introduced by Scala as the target programming language:
-
Scala provides a special notation for Getters and Setters. However in the current reference implementation of the Synchronization this is not used. Instead fields are accessed by Getters and Setters in the usual Java-way.
-
The same goes for constructors - there are multiple ways of defining constructors and different notations have to be used. Again we do not consider these and only create one full constructor.
The necessary preparations will be carried out through the visitors mentioned in
Modifying SModel
instances. In short there are two visitors used: the
GetterSetterGeneratingVisitor
for general setup and the SyncEnhancingVisitor
for the
modifications specific to synchronization. It is important to run the general visitor first to
ensure that all classes contain setter methods. These will than be augmented by the second visitor.
In order to integrate models into the Synchronization demo, configuration classes are necessary. The Generator does currently not create those, as they need application specific knowledge. At some point in the future this information may supplied as ATL files. |
Writing to the File system
The process of compiling and packaging an SModel
is pretty straightforward: all SClasses
need
to be written to the file system. Afterwards the Scala compiler may be invoked and the resulting
byte-code files will be put into a JAR-file.
For executing this workflow a visitor will be used once again. The SModelFSWriter
will write each
SClass
to the file system upon each visit
. Once the whole SModel
is being visited, the visitor
knows it is done and starts compilation as well as packaging. During this step, other resources
such as the model diagram images will be copied as well.
Although the general steps are quite simple, some details do introduce difficulties. Namely copying files into a JAR may only be done by streaming the data into the JAR. Furthermore setting up the compiler requires the classpath to be adapted to the current environment.