Using the built-in code generator
Code generation for UML models
Code generation for OCL expressions
Code patterns used and limitations
The main objective of the source code generator is to produce a completely runnable application from the UML model and the attached OCL specifications. Using the built-in code generator, you can turn classes, interfaces, datatypes into their corresponding Java source code equivalents. In its current version, OCLE also generates the code required for the runtime management of associations. Additionally, attached OCL constraints are turned into runnable code, which is integrated in the code for the UML model, producing a Java application that checks its own constraints. Thus, apart from the verification capabilities provided by the evaluation system (see model checking and debugging), you may also check your constraints at runtime. Unlike in other state-of-the-art tools, you can generate the code for the operations specified in OCL (using the def-let mechanism). The code generated for OCL specifications reduces object allocation to the maximum extent possible, using built-in Java types wherever this is possible.
The code generator produces Java source code that is compilable, though not complete. Once you have generated the code, you still have to write the body of generated methods.
To invoke the built-in source code generator, use the Generate code... option in the Tools menu. Please note that this option is only active if you have an active user model loaded in OCLE. For details about UML models and OCLE projects, you may want to refer to UML models and project management. The code generation wizards starts, showing the window in figure 1.
Fig. 1 Code generation wizard |
You first have to specify the destination directory. The destination directory is where your source files are placed, in fact the
base directory for the classpath of the resulting application. Use the Browse button to locate a specific directory. The value of the destination
directory property remains unchanged between two generation operations, so for
the same project you will have to specify this directory only once. However,
you will probably have to change it when you switch to another OCLE project
in the same session. At first code generation operation, the supplied
default directory is <STARTUP_DIRECTORY>/output
.
In the code generation wizard you may also provide details about the properties of the generated code. Thus, select the Put opening brace on new line option if you want the opening brace '{' to be placed on the next line for class, method and block declarations. This checkbox is not selected by default, to follow the Java coding conventions.
When generating code, OCLE inserts one tab as the indentation level increases. Therefore, you may also specify if tabs must be converted to spaces and the number of blank characters for a tab (the default is 4 spaces).
The most important option provided by the code generation wizard is the validation of business constraints (the Validate business constraints option). This checkbox can only be selected if there is an active project. Therefore, it is advisable that you define an OCLE project containing the UML model and some business constraints before generating code. If this checkbox is selected, OCLE will translate the associated business constraints into equivalent Java code. Moreover, this code will be injected in the code for the active UML model, thus creating a running application that checks its own constraints. Unselect this checkbox if you are interested only in the code for the UML model.
The source code generator allows you to turn design models into skeleton code. Skeleton code consists of the structure code for all model entities that have an equivalent in the target programming language (Java in this case). The goal of the generator is to translate most of the model elements in their corresponding source code equivalents. Different tools allow for different types of model elements to be turned into code. This version of the code generator considers only the elements composing the static view of the model. Therefore, no code is generated for actors, object diagrams or state machines. Instead you may generate code for the classes, interfaces, data types and enumerations in the active user model. An outstanding capability is the management of accessor methods for associations, even qualified.
The code generation process itself has two phases. The first consists in the validation of the active UML model against a set of profile rules. Currently, two profiles are considered: the UML profile and the Java profile. The UML profile specifies the rules that the model must fulfill in order to be correct against the definition of the UML (well formedness rules). The Java profile imposes additional, Java specific rules, such as for example no feature may have a Java reserved word as its name. The model checking process is basically a batch evaluation process, since profile rules are expressed in OCL. Therefore, any model errors are reported in the same manner as with plain batch evaluation. If at least one error is identified, you will be prompted to confirm that you want to go further with code generation, because the resulting Java code may have syntactic / semantic errors. However, at this step you may choose to stop the code generation process.
Below you can find some details regarding the way in which the code generator handles different types of model elements.
An interface in your UML model is turned into a Java interface. If the interface is created with OCLE (not imported from some other tool through XMI), you can only add operations to it. These operations are translated to method prototypes in the generated Java interface. Note that any existing visibility modifier is ignored, since methods in Java interfaces are by default public (please see the Java language specification). If your interface happens to contain some attributes (if it is imported from another tool), these attributes are turned into the corresponding Java fields. No visibility modifier is inserted either, since fields in Java interfaces are by default public, static and final. Therefore, we strongly suggest that you pay special attention to the semantics of the fields declared in interfaces when you want to generate source code for them.
For classes, the following rules apply. First, each attribute is translated into the corresponding Java field. The owner scope and visibility properties of the attribute specify whether the resulting field is static and public/protected/private/friendly. For operations, besides the prototype, a default return statement is generated if any non-void type is specified as the operation's return type.
Associations have a special treatment. Each navigable opposite association
end is turned into a Java field. For association ends with multiplicity greater
than one, the java.util.Set
type is used as the declared type of the field.
Note that this behavior is not customizable as of this version of OCLE, which
means that you cannot change the container type for association ends.
If an opposite association end of a class or interface is part of an association class, the following rules apply:
java.util.Set
Thus, in the Company example (provided with the distribution), for the (partial) class diagram in figure 2, you will see the following sources (only fragments are included, for brevity)
Fig. 2 Example class diagram for code generation |
//File Person.java //the declaration for the opposite end 'employer' public Set employer; ... public final Set getEmployer() { if (employer == null) { return java.util.Collections.EMPTY_SET; } return java.util.Collections.unmodifiableSet(employer); } public final Set directGetEmployer() { java.util.Set temp = new LinkedHashSet(); if (employer != null) { Iterator it = employer.iterator(); while (it.hasNext()) { temp.add(((Job)it.next()).getEmployer()); } } return temp; } public final void addEmployer(Job arg) { if (arg != null) { if (employer == null) { employer = new LinkedHashSet(); } if (employer.add(arg)) { arg.setEmployee(this); } } } public final void removeEmployer(Job arg) { if (employer != null && arg != null) { if (employer.remove(arg)) { arg.setEmployee(null); } } }
No matter the multiplicity of the opposite
association end, the Java class generated for the association class will also
contain two fields having the name and type corresponding to its connections as
a plain association. In the diagram above, it is the case of the Job
class.
//File Job.java public Company employer; public Person employee; public final Person getEmployee() { return employee; } public final void setEmployee(Person arg) { if (employee != arg) { Person temp = employee; employee = null; //to avoid infinite recursions if (temp != null) { temp.removeEmployer(this); } if (arg != null) { employee = arg; arg.addEmployer(this); } } }
If an opposite association end of a class is qualified (has at least one qualifier), the resulting Java field
will be declared of type java.util.Map
. This type cannot be changed in this version of OCLE.
For each association end, besides the corresponding Java field, OCLE also generates the minimum required accessor
methods. Their purpose is to enable the developer maintain the consistency of
links between the objects composing the runtime configuration. Basically, this is
about updating links in both directions, irrespective of the accessor method
that triggers the update operation. For example, the removeEmployer(Job)
method
in class Person
also calls setEmployee(null)
on the argument object.
For simple, non-qualified association ends, only two methods are generated. The getter method returns either the value of the
association end or an unmodifiable view of the set that contains the objects at
the opposite link end, depending on the end's multiplicity. The setter has the
form set<EndName>
or add<EndName>
if the end is 0..1/1 or multiple respectively. If the association end is
multiple, an additional remove
method is automatically generated.
For an association end that is part of an
association class, the setter methods have the same characteristics. However, a
supplementary getter is generated that obtains the real opposite end, skipping
the association class object. This is the case of the directGetEmployer()
method in class Person
.
In the case of association classes, connections are mapped to fields of multiplicity 0..1/1, and appropriate
setter / getter methods are generated (see the Job
class code snippet above).
For qualified association ends, the signature of generated accessor methods also includes the qualifier attributes. Particularly, two getter methods are generated. One of them has no arguments and returns the entire set of objects at the opposite association end. The other returns a single object from the set. This object is retrieved based on specific values for the qualifier attributes. Therefore, this second getter method has the qualifiers as formal arguments. For example, the structure displayed in the diagram in figure 3 turns into the following code (only an excerpt included, for brevity)
//File Bank.java (class Bank) public final Set getCustomer() { java.util.Set temp = new LinkedHashSet(); if (customer != null) { temp.addAll(customer.values()); } return temp; } public final Person getCustomer(int accountNumber) { if (customer == null) return null; ArrayList key = new ArrayList(); key.add(Integer.toInteger(accountNumber)); return (Person)customer.get(key); } public final void addCustomer(int accountNumber, Person arg) { if (arg != null) { ArrayList key = new ArrayList(); key.add(Integer.toInteger(accountNumber)); if (customer == null) customer = new HashMap(); Person temp = (Person)customer.put(key, arg); //the previous value, if any if (temp != arg) { arg.setBank(this); if (temp != null) { temp.setBank(null); } } } } public final void removeCustomer(int accountNumber) { if (customer != null) { ArrayList key = new ArrayList(); key.add(Integer.toInteger(accountNumber)); Person temp = (Person)customer.remove(key); if (temp != null) { temp.setBank(null); } } } public final void removeCustomer(Person arg) { if (customer != null || arg != null) { if (customer.values().remove(arg)) { arg.setBank(null); } } }
Fig. 3 Example class diagram for qualified associations |
A qualified association end inherently has a
multiplicity greater than one. Therefore, additional
add
and remove
methods are generated for it. The generated add
method has the qualifiers specified as formal arguments, the last argument being
the object to add, declared with the appropriate type.
There are two remove
methods generated. One of them also has the qualifiers specified as formal
arguments. The other specifies the exact object to remove. Please see the above code snippet for the qualified association
between class Bank
and Person
.
Some UML CASE tools (Rational Rose, for example) create Datatypes behind the scenes and the resulting models refer to these datatypes. Therefore, a proper mapping is required for datatypes too. When it encounters a Datatype, OCLE first checks if it represents a primitive type. If it is a non-primitive type, a Java class is generated for it, using the mapping for plain UML classes.
Since enumerations are very useful artifacts both in modeling and in programming languages, OCLE also defines a
mapping for the Enumeration
metaclass. This mapping is also used for classes that are <<enumeration>> stereotyped.
For an explicit enumeration, a class is generated that has the literals as public static final fields. These fields are initialized
in a static initializer using a private constructor. This behavior is the same for <<enumeration>> stereotyped classes; the only difference is that
the fields are generated from the features of the class (instead of the enumeration literals). Please note that the resulting fields are declared
as having the class / enumeration itself as their type. Moreover, for <<enumeration>> stereotyped classes, any operations and any field type/
visibility specifications are ignored. Therefore, we stronly suggest that you pay special attention to which classes you apply the <<enumeration>>
stereotype when you intend to generate Java code from them.
Consider the enumeration Sex
in figure 4 (this is from the Company example included in this distribution of OCLE).
Fig. 4 Enumerations in the model browser |
Below you find the generated source code for enumeration Sex
.
public class Sex { private Sex() { } public static final Sex male; public static final Sex female; static{ male = new Sex(); female = new Sex(); } }
As you may see, the standard equals()
method is not generated for the corresponding Java class. This means that enumeration literals will be compared
using object identity. This is not a problem, however, since the corresponding class has a private constructor, and the only instances are those created
in the static initializer - in fact the enumeration literals.
If the Validate business constraints checkbox is selected, OCLE will also generate source code equivalents for the OCL expressions attached to the active user model. Moreover, the resulting code fragments are properly integrated in the source code for the structure of the model. Note that only business constraints are taken into account, which means that no code is generated for OCL specifications expressed at the metamodel level.
In its current version, OCLE translates the following entities:
- Class invariants
- Method preconditions and postconditions
- Observer operations , specified using the def-let mechanism
OCL 2.0 tuple types are also treated in a special manner by the code generator.
Following paragraphs describe the way in which different types of OCL specifications are integrated into the generated structural code. The extensions to the OCL grammar proposed by us (see them here) and implemented in the integrated OCL compiler are not taken into account by the source code generator.
This section describes the ways in which the source code generated for different kinds of OCL expressions is integrated into the generated structural code.
If a class in your UML model has at least one invariant attached to it, its generated source code is modified in the following way:
- A public inner class, having its name starting with
ConstraintChecker
is declared. This class is responsible for the validation of class invariants at runtime.- As of this version, no code is generated that uses this class (no insertion points are defined for any class invariant). The developer must decide upon where to trigger the verification of a class invariant.
- The constraint checker class inherits either from a default class provided by the framework if the constrained class has no parents or its parents are not constrained, or from the constraint checker class attached to the parent class, if the parent class is also constrained. Invariant inheritance is ensured in this way.
- The constraint checker class has a public method,
checkConstraints
, which is responsible for triggering the verification of all class invariants, including any inherited invariants.- For any specific class invariant, a public method that verifies it is inserted in the constraint checker class. The
checkConstraints
method contains one call for each such method.
Thus, wherever the Java compiler allows it, you may trigger the verification of one or more class invariants using the code template given here:
new ConstraintChecker().<checker_method>();
where <checker_method> is either checkConstraints
for all constraints or a specific method for checking a particular invariant. Since the ConstraintChecker
class is public, you may trigger the evaluation
of class invariants even outside the body of the constrained class.
For operation constraints, OCLE uses the same code pattern as for class invariants. Some differences exist, however. The constraint checker class is inserted
at the beginning of the body code for the constrained method. It has no base class. It contains two public methods, checkPreconditions
and
checkPostconditions
. Both methods have the same arguments as the constrained method itself, so that the names of the formal parameters
are available to the code that checks the constraints. The constraint checker class also contains a private checker method for each method constraint.
The resulting private methods are called by either checkPreconditions
or checkPostconditions
.
The code for a constraint method looks like this:
public boolean hireEmployee(Person p) { class ConstraintChecker { public void checkPreconditions(Person p) { check_precondition(p); } public void checkPostconditions(Person p) { } public void check_precondition(Person p) { Set setEmployee = Company.this.directGetEmployee(); boolean bIncludes = CollectionUtilities.includes(setEmployee, p); boolean bNot = !bIncludes; if (!bNot) { System.err.println("precondition 'precondition' failed for object "+Company.this); } } boolean result; } ConstraintChecker checker = new ConstraintChecker(); checker.checkPreconditions(p); checker.result = internal_hireEmployee(p); checker.checkPostconditions(p); return checker.result; }
As we can see, the real body of the constrained method is moved
in another method (in this case internal_hireEmployee()
). Moreover,
the constraint checker class also has a public field, result
which
maps to the result variable available in OCL postconditions. This variable
is properly assigned before triggering the verification of any postcondition.
However, some postcondition specific constructs, such as @pre and the oclIsNew operation in OclAny
are not
properly mapped in the generated source code. If the code generator encounters
such constructs, it issues adequate warning messages.
All OCL observers are mapped to Java methods in the generated source code. The corresponding method for such an observer (somertimes called a definition constraint ) is public and is placed in the class in the context of which the definition constraint is specified. The following example (Finance Business, also included in the distribution) illustrates the translation of definition constraints.
context Portofolio def prices: let priceOfOrders: Real = orders.security.price->sum()
In this case, class Portofolio
would contain the following code (completely generated by OCLE):
public float priceOfOrders() { Set setOrders = Portofolio.this.getOrders(); //evaluate 'collect(security)': List bagCollect = CollectionUtilities.newBag(); final Iterator iter = setOrders.iterator(); while (iter.hasNext()) { final Order decl = (Order)iter.next(); Security securitySecurity = decl.getSecurity(); bagCollect.add(securitySecurity); } bagCollect = CollectionUtilities.flatten(bagCollect); //evaluate 'collect(price)': List bagCollect0 = CollectionUtilities.newBag(); final Iterator iter0 = bagCollect.iterator(); while (iter0.hasNext()) { final Security decl = (Security)iter0.next(); float fPrice = decl.price; bagCollect0.add(Real.toReal(fPrice)); } bagCollect0 = CollectionUtilities.flatten(bagCollect0); float fSum = CollectionUtilities.sum(bagCollect0); return fSum; }
If your business constraints use OCL tuple types, OCLE will turn each identified (distinct) tuple type into a class. Tuple parts are converted to public fields with
the names deduced from the names of the involved tuple parts. This mapping is in fact similar to C/C++ structs. The equals()
method is also generated,
to ensure proper comparison between tuples (tuple type instances). Let us for example consider the following OCL tuple type:
TupleType(array:String, length:Integer)
OCLE will generate the following code for it:
//File TupleType1.java package tupleTypes; publc class TupleType1 { public boolean equals(Object arg) { if (!(arg instanceof TupleType1)) { return false; } boolean result = true; TupleType1 local = (TupleType1)arg; result&= (array != null ? array.equals(local.array) : local.array == null); result&= (length == local.length); return result; } public String array; public int length; }
Please note that the name of the resulting class is generated by OCLE, since OCL does not allow for the specification of tuple type names.
All tuple types identified in the OCL expressions are placed in a dedicated package, named tupleTypes and this behaviour cannot be changed in this version of OCLE - this means that you cannot change the location for a class corresponding to a tuple type.
The translation from OCL to Java follows a syntax-directed approach. Support for metamodeling (OclAny
, allInstances
and so on)
is provided by means of a simple library shipped with the distribution.
The library, named OCLFramework.jar is located in the /lib subdirectory of your OCLE installation directory.
Since the generated code contains calls to classes in the framework, you will have to include this archive in the classpath of
the generated application. Basically, the entire OCL 2.0 grammar is supported. There are however two notable drawbacks. First of them
has already been mentioned: the @pre construct is not properly mapped. Moreover, this version provides limited degree of customization as
far as constraint violations are concerned. More precisely, the same pattern, based on System.err.println
is used whenever a constraint is violated. Additionally, although supported by the compiler, messaging expressions are not translated
in equivalent source code.
The source code generator was designed with efficiency in mind. Therefore, primitive OCL types are mapped to primitive types provided
by the Java programming language, thus avoiding excessive wrapping. More precisely, Boolean
maps to boolean
, Integer
maps
to int
and Real
maps to float
. The String
type provided by OCL is
translated using the java.lang.String
type. Please note that these mappings are not customizable as of this version of OCLE.
The OclAny
metaclass is equated with the root class java.lang.Object
.
The operations in OclAny
, including the navigation to OclType
are translated by redirecting them to
the Ocl
class in the above mentioned library. Each method in this class has an object as its first argument. This
argument is a placeholder for self
. The rest of the arguments are those mentioned in the OCL specification.
Whenever an object is expected and the previous result is of primitive type, the expected object is obtained by wrapping the
primitive value. Please note, however, that some operations are not properly implemented in this version (this is the case for
oclIsNew()
and oclInState()
).
The OclType
metaclass in OCL is mapped to a class OclType
specifically designed for this purpose
in the OCLFramework
library. Basically, OclType objects are wrappers around java.lang.Class
objects.
However, OclType
can be used to model nested collection types, such as Set(Bag(Integer))
. The following
table lists the mappings used for OCL collection types.
OCL Collection type | Java equivalent type |
Set | java.util.Set |
OrderedSet | ro.ubbcluj.lci.codegen.framework.dt.OrderedSet |
Sequence | java.util.List |
Bag | java.util.List |
Type casts and conversion to primitive values are generated whenever necessary, so that the resulting source code is semantically correct from the point of view of the Java compiler
Operations in OCL collection types are translated by redirecting them to the framework class CollectionUtilities
, as
in the case of OclAny
. As in that case, the first of argument of the methods in the CollectionUtilities
class is a placeholder for
self - the collection on which the operation is called.
This version of the code generator does not handle OCL Undefined values properly. The biggest problem we encountered when modeling
the Undefined
value was its type, OclVoid
. The OCL specification asserts that this type must
conform to all types in the user model or in the OCL datatype system. Therefore, the code generator does not map the OclVoid
type explicitly. Instead, the semantics mentioned in the OCL grammar is partially ensured using the following conventions:
- The null literal is used as a placeholder for
Undefined
in the case of non primitive types.Integer.MAX_VALUE
is used as a placeholder for Undefined integer values andFloat.POSITIVE_INFINITY
is used for undefined real numbers. As a consequence, undefined boolean values cannot be modeled in the generated code. Due to this limitations, you may encounter some compilation problems in the generated source code.
Back to main index |