The Meta-Object Facility (MOF) Query/View/Transformation (QVT) specification includes three languages: the Relations language, the Core language, and the Operational Mapping Language (OML). The first two are related, in that the Relations language is transformed into the Core language for “execution.” The Relations language is a high-level, declarative language that is intended to be more user-friendly than low-level, imperative languages. It can support complex pattern matching between objects, creating a trace file implicitly that allows for bidirectional transformation.
The Core language is semantically equivalent to the Relations language but is defined at a lower level of abstraction. Therefore, transformations written in Core are more verbose than those written in Relations. Trace models must be defined explicitly, unlike in Relations, where they are derived from the transformation definition.
Both the Relations language and OML are currently under development within the Model-to-Model Transformation (M2M) project. At the time of this writing, only the OML is available, so it is the focus of this book. Note also that this book focuses on aspects of OML that have an implementation available, but it also discusses some aspects of the language that are planned to be supported in the future.
The OML is intended to provide an imperative alternative, which can be invoked from the Relations and Core languages in a “black-box” manner. OML makes extensive use of Object Constraint Language (OCL), which this book does not cover in detail. As with Eclipse Modeling Framework (EMF), OCL is covered by a book of its own, which is recommended reading before, or in parallel with, this book. This chapter covers the extensions to OCL added in the QVT spec.
OML is an imperative language that most programmers will find familiar. The language is used to define unidirectional transformations, although a second transformation can always be written to provide bidirectionality. It can provide implementations of mappings for the Core or Relations languages when declarative approaches prove difficult, which is known as a hybrid approach. Transformations defined exclusively using OML are known as operational mappings.
As you will see in the language description that follows and throughout the examples in this book, you can write equivalent QVT scripts in many ways. The decisions made by the transformation author to use particular techniques and constructs will be determined by experience, style preference, reusability, and maintainability factors. As the language and tooling support matures, features for navigation, refactoring, and optimization of QVT scripts are expected to improve the experience of working with QVT from its current state.
A QVT transformation is defined in a file that includes a transformation signature and main
mapping to serve as an entry point. One or more modeltype
declarations can also be included to explicitly define the metamodels used in the transformation. Following is a transformation declaration with modeltype and main mapping elements:
As you can see, a transformation definition is much like a class declaration, with import statements, a signature, and a main()
mapping entry point. The analogy between operational QVT and object-oriented languages is accurate: Transformations are instantiated and have properties and (mapping) operations. Note that the mappings of the transformation are enclosed within curly braces following the transformation signature. This is not required if the file has only one transformation, in which case the transformation declaration is terminated by a semicolon. At the time of this writing, QVT Operational (QVTO) currently supports only one transformation per file, so the previous syntax would result in a compiler error. Instead, declare transformations as follows:
transformation uml2rdb(in
uml:UML,
out rdb:RDB);
Each model parameter is associated with an MOF extent. The transformation itself can be considered a class that is instantiated, causing the initialization of the parameter extents. Output parameters are initialized to empty model extents. All are accessible using the this
variable, which refers to the transformation instance itself. When instantiating objects, the model extent can be declared to remove ambiguity when multiple models of the same type are in use. This is done using the @
sign followed by the name, as shown here:
The modeltype
declaration assigns an alias to a metamodel used in the context of the transformation. The uses
part of the declaration specifies the model name and registered URI that the environment uses to resolve the MOF metamodel definition. In the case of Eclipse QVT OML, MOF is equivalent to EMF’s Ecore metamodel. Therefore, URIs used here are those found in the NS URI field of packages registered in EMF. To see a list of the models registered in the environment, use the Metamodel Explorer view provided by the M2M OML component. It’s also possible to specify modeltypes using Eclipse platform:/plugin
and platform:/resource
Uniform Resource Identifiers (URIs).
Note that it’s not necessary to place the URI within parentheses following the model name. The following is equivalent to our Unified Modeling Language (UML) modeltype declaration earlier:
modeltype UML uses 'http://www.eclipse.org/examples/1.0.0/simpleuml';
You also can simply state the model name, which, in the case of M2M OML, corresponds to the EMF registered package. This allows some flexibility because several versions of a model could be registered in the environment, but it also can cause some conflicts because you cannot guarantee that the model name is unique. Technically, the NS URI is no guarantee of uniqueness, but it gives a much higher degree of confidence. Following is another modeltype declaration that uses just the model name:
modeltype UML uses simpleuml;
A modeltype declaration can also include a strict
qualifier. By default, model types are effective and flexible, in that they allow the transformation to work with similar model types. For example, slight version changes of a metamodel might not impact a QVT definition, so the effective
declaration allows these instances to be processed. If the transformation author requires a specific metamodel to be used, adding strict
before the uses
clause provides the required enforcement. For example, this is a fully qualified modeltype declaration using strict
compliance:
modeltype UML 'strict' uses
simpleuml('http://www.eclipse.org/examples/1.0.0/simpleuml'),
You also can restrict the model type using a where
clause. For example, the following declaration imposes the restriction that a model must have at least one Class
. This capability allows for a degree of validation of input models without executing the transformation.
modeltype UML uses
simpleuml('http://www.eclipse.org/examples/1.0.0/simpleuml') where
{self.ownedElements->closure(oclIsKindOf(UML::Class))->size() > 0};
Currently, the M2M QVTO implementation produces a warning that metamodel conditions are not supported.
The previous signature states that the uml2rdb
transformation will take as an input a uml model instance of type UML and return an rdb model instance of type RDB. From the modeltype
statements, we know that these map to models found in our environment by name or URI. Parameters have a direction (in
, out
, or inout
), an identifier, and a type. Parameters that are designated as in
parameters are not changed, those designated as inout
are updated (these are sometimes referred to as in-place transformations), and those designated as out
are assigned the newly created result. Note that it’s possible to have abstract transformations, where the main()
mapping is disallowed.
It is possible to invoke a transformation for the purpose of modifying an existing model. Potential uses for this capability are model cleansing, refactoring, or refinement. Section 6.2, “Model Refactoring,” provides an example of an in-place transformation. Part of this example is provided again here:
The key to using QVT for in-place transformations is to use inout
declarations in the transformation signature, main mapping, and mapping definitions that modify the model.
A transformation can extend another transformation or library. For example, suppose that we have a BaseUml2Rdb
transformation and UmlUtil
library that we want to extend. Furthermore, we want to access a typeUtil
library. To indicate this, we simply add the following extends
and access
keywords with transformation names to the end of our signature. In the case of extension, mapping definitions can override those in the extended transformation.
transformation uml2rdb(in uml:UML, out rdb:RDB)
extends BaseUml2Rdb
extends library UmlUtil
access library typeUtil;
At this time, M2M OML does not support access and extension of transformations and libraries. The M2M project does not yet have a Relations language implementation, so it also does not yet support the refines
keyword.
OML supports the definition of intermediate
classes and properties, which can be helpful in some transformation definitions. Essentially, the definition of an intermediate
class and associated property allows for metamodel extension in the context of the transformation. The example used in the specification follows; here, a LeafAttribute
class is defined to help with mapping complex type attributes to relational database columns. An intermediate
property allows for the storage of LeafAttributes
in a Sequence
, accessible as a feature of the Class
element.
intermediate class LeafAttribute {
name: String;
kind: String;
attr: Attribute;
};
intermediate property Class::leafAttributes: Sequence(LeafAttribute);
Elsewhere in the transformation, the leafAttributes
property can be accessed in the same manner as any other feature of the metamodel. For example:
self.leafAttributes:= self.attribute->map attr2LeafAttrs();
Unfortunately, intermediate classes are not yet implemented in Eclipse M2M OML. Intermediate properties are supported and can be a Tuple type, providing somewhat of a workaround for the lack of intermediate classes, as shown next. Another workaround is to define an intermediate class in a separate *.ecore
model and use it within the context of the transformation.
intermediate property UML::Class::leafAttributes:
Sequence(Tuple(name:String, kind:String, attr:Attribute));
External properties might need to be passed into a transformation, which you can accomplish using a configuration property
declaration. For example, say that you are transforming a model into a Java model and need to specify the package namespace, but it’s not a property in the source model. Assuming that you know this value at invocation time, it can be passed to the transformation and set in a configuration property. A name and type are required, as shown in the following example.
configuration property namespace: String;
Using Eclipse OML, the launch configuration dialog has a Configuration tab that is aware of all configuration property entries in the specified transformation. Values entered are passed into the executing transformation instance to initialize the corresponding properties.
Sometimes a clash arises between the name of elements in a metamodel and, say, keywords in OML. In this case, the rename
facility can provide an alternative naming in the context of the transformation. In the following example, the library
attribute of the java::Class
element is renamed to lib
to avoid a clash with the library
keyword:
rename java::Class.lib = 'library';
This feature of the QVTO implementation is not generally required because name clashes are handled automatically by prefixing the element with an underscore. So using the previous example, the library
element would be accessed as _library
within the transformation.
Within the context of a transformation definition, a number of predefined variables are available. You can access the transformation itself, or the instance thereof, using the this
variable. You can therefore use this
to access configuration properties, mappings, helpers, and so on. For example, to reference a property defined at the transformation level, you can access it as follows:
this.dependencies:= mmap.dependencies();
As you will see shortly, within the context of a mapping or query, the contextual parameter is accessed using the self
variable. The final predefined variable, also discussed in detail soon, is the result
variable, which accesses the result parameter or tuple of a mapping or helper.
Within the context of a transformation, the literal null
complies to any type and is used to mean the absence of value. It can be used as the return of an operation, either explicitly or implicitly. From OCL, the type OclAny also represents an object of any type, while the type OclVoid represents an undefined value and conforms to all types. The OclAny operation oclIsUndefined returns true
when its argument is undefined.
Often you can reuse query, mapping, and type definitions in transformations. When this is the case, they are defined in library
modules and imported as discussed earlier using access
or extends
statements in a transformation signature. Using access
implies import semantics, whereas extends
implies inheritance semantics.
The main differences between a library and a transformation are that no main
entry point is defined for execution in a library and that models listed in its signature are those it operates on, not parameters. Following is a library definition UmlRdbUtil
that operates on UML model instances, extends the UmlUtil
library, and accesses the RdbUtil
library:
library UmlRdbUtil(UML)
extends UmlUtil
access RdbUtil;
Note that QVT defines a standard library StdLib
that is implicitly imported in every transformation definition. This is similar in concept to the java.lang.*
package, which is imported automatically in every Java class.
OML mapping
operations are the refinement of a relation and provide the fundamental behavior of transformations. Mappings take one or more source model elements and return one or more target model elements. Following is the general syntax of a mapping operation, where <direction> is one of in
, out
, or inout
. A mapping is either contextual, as seen here, where X represents the type and prefixes the mapping name, or noncontextual, where the input parameter is explicitly declared, as in mapping XtoR(in x : X): R
.
A mapping
has a name, which, by convention, follows either an X::<input_element>2<output_element>()
or X::to<output_element>()
pattern, where X
represents the input element type. The mapping
name is prefixed by the fully qualified input element, separated by double colons. This is standard OCL namespace syntax. The input object is referenced using self
within the mapping
. Note that a mapping can be declared as abstract.
Parameters are comma separated and indicate direction in|out|inout
followed by name and type information. Input parameters cannot be modified, while inout parameters can be updated. The out
parameter receives a new value but cannot be newly created if a previous mapping invocation for the input instance has been processed. The following example specifies a single input parameter named targetType
and conforms to the type UML::DataType
. In this case, UML is defined by a modeltype
declaration at the top of the transformation.
The result is declared following a single colon after the parameter list. In the following example, the return conforms to the type RDB::TableColumn
, where RDB is also defined by a modeltype
declaration. Note that it’s possible to have multiple results for a mapping. The result
keyword is used to reference the return object, or tuple, in the case of multiple result objects.
Before the mapping body are optional when
and where
sections, both of which evaluate contained Boolean expressions. The when
clause acts as either a precondition or guard, while the where
clause acts as a post-condition. In the previous example, the input Property object is validated as being primitive using a call to the isPrimitive()
query. If the when
clause evaluates to false
, the mapping is not executed and a return object is not created (null
is returned). If the mapping is invoked using strict semantics (xmap), the mapping is not executed and an exception is thrown. Currently, M2M OML does not support the where
clause.
Within the body of the mapping, marked by open and closing curly braces, are optional init
, population
, and end
blocks. These are optional, in that init
and end
blocks are not required, and the population
section is implied as the remaining area within the mapping body itself. The specification describes situations when it is required to use an explicit population
section, but it’s not generally used.
The init
section is where computation can take place to initialize variables, explicit setting of output parameters, and so on before the effective instantiation of the mapping output. A possible use for init
is to instantiate an object that is a subtype of that defined as the result in the mapping definition. The output values are set in the population
section, and finalization of computation occurs in the end
section before the mapping returns.
You can consider yet another section to exist implicitly between the init
and population
section where return objects that are not initialized are instantiated. What’s important to realize here is that if you want to assign an existing object to a return parameter, you must do this in the init
section. The end
block is a finalization section for placing additional code that must be invoked before the mapping operation returns.
To better understand a mapping operation’s execution semantics, consider the following sequence:
1. When resolved, a mapping is executed with all parameters passed as a tuple. This includes the context parameter (first) and the result parameter (last).
2. All out
parameters are initialized to null
, while all input parameters are passed by reference.
3. The type compliance of all parameters and evaluation of the when
clause takes place. If failure occurs, null
is returned.
4. If the guard succeeds, the trace is consulted to see whether the mapping has previously been satisfied for the given input. If so, the out parameters are populated using the previous result and are returned. Otherwise, the mapping body is entered.
5. The expressions found in the init
section (if present) are executed in sequence.
6. Following init
, the “implicit instantiation” section is entered, where all output objects are initialized, if still null
. Collection types are initialized with empty collections, and trace data is recorded for the mapping.
7. Each expression in the population section is executed in sequence, typically operating on the out
or inout
objects.
8. The end
section expressions are then executed in sequence.
The execution semantics change when a mapping inherits, merges, or is a disjunction of another mapping, as described in Section 13.3.3, “Inheritance, Merger, and Disjunction.”
During execution of a mapping operation or helper, you can control the flow using explicit return
statements. If a value is provided, it is assigned to the result
object of the mapping.
A special form of mapping, known as the entry operation, is marked with the main
keyword. There can be only one main
per transformation—or, in the case of abstract transformations, no main
operation at all. If used without mapping
, this entry point takes no parameters and has no init
, population
, or end
block. A main
can be used with the mapping
keyword and combines the aspects of the transformation entry point and those of a regular mapping operation. Following is a main
that does not declare that it’s a mapping operation. The output is assigned the result of the toRequirementsModel()
mapping. As you can see, QVTO allows parameters in main
operations, although it’s technically not allowed in the specification.
main(in mmap: mindmap::Map@inModel, out req:
requirements::Model@outModel) {
req:= mmap.map toRequirementsModel();
}
An example of using main
without parameters follows. Strictly speaking, this is what all main()
operations should look like. In most cases, the in
model parameter is accessed directly and is used to invoke the mapping operation. The rootObjects()
operation is available on all Model objects. The use of brackets in the statement is an example of the collectselect
statement shorthand.
transformation mindmap2requirements(in inModel: mindmap,
out outModel: requirements);
main() {
inModel.rootObjects()[Map]->map toRequirementsModel();
}
Mappings can extend other mappings through inheritance, can have their result merged with the result of another mapping, or can be executed based on the success of their guard conditions. Each of these is discussed next, along with how they impact the execution semantics at runtime.
Mapping operations can inherit from other mapping operations. During execution, the inherited mapping is invoked after the initialization section of the inheriting mapping. This includes the implicit instantiation section. The effect of this is that output parameters are non-null
when the inherited mapping is invoked.
Following is an example of inherits
used in the context of our dnc2jee
.qvto
transformation found in Section 6.9, “Transforming a Business Model to Java.”
mapping dnc::Archetype::toClass(): java::JavaClass {
name:= self.name;
fields += self.getAttributes().map toField(result);
methods += self.getOperations().map toMethod();
}
mapping dnc::Archetype::toSerializableClass(): java::JavaClass
inherits dnc::Archetype::toClass {
implementsInterfaces += 'java.io.Serializable'.map toClass();
}
mapping dnc::Archetype::toStateful(): java::JavaClass
inherits dnc::Archetype::toSerializableClass {
eAnnotations += toAnnotation('description', self.description, null);
eAnnotations += toAnnotation('annotation', '@Stateful', null);
classImport += 'javax.ejb.Stateful'.map toClass();
}
In this example, toStateful()
inherits from toSerializableClass()
, which, in turn, inherits from toClass()
.
Sometimes a mapping operation produces multiple outputs or results in an object that is the logical combination of other defined mappings. By allowing for the merging of mapping operations, the language of the transformation more closely approximates a natural language. In terms of execution semantics, the merged mappings are invoked following the end of the merging mapping. All mappings, including out
, are passed to the merged mapping.
We can modify our previous inherits
example to include merges
for the serializable aspect of the stateful class. In this case, a stateful bean is a class that also is serializable.
mapping dnc::Archetype::toClass(): java::JavaClass {
name:= self.name;
fields += self.getAttributes().map toField(result);
methods += self.getOperations().map toMethod();
}
mapping dnc::Archetype::toSerializableClass(): java::JavaClass {
implementsInterfaces += 'java.io.Serializable'.map toClass();
}
mapping dnc::Archetype::toStateful(): java::JavaClass
inherits dnc::Archetype::toClass
merges dnc::Archetype::toSerializableClass {
eAnnotations += toAnnotation('description', self.description, null);
eAnnotations += toAnnotation('annotation', '@Stateful', null);
classImport += 'javax.ejb.Stateful'.map toClass();
}
Another option when structuring mappings is to specify several mappings from which the first that satisfies its guard conditions (type and when
clause) is executed. This is done by specifying a list of mappings after the disjuncts
keyword in the mapping declaration.
During execution, each guard is executed in a series until one is satisfied. The first successful disjuncted mapping is executed. If none of those listed satisfies the conditions, null
is returned. Following is an example adopted from the specification, for illustration:
mapping uml::Feature::convertFeature (): java::Element
disjuncts convertAttribute, convertOperation, convertConstructor {}
mapping uml::Attribute::convertAttribute: java::Field {
name:= self.name;
}
mapping uml::Operation::convertConstructor: java::Constructor
when {self.name = self.namespace.name} {
name:= self.name;
}
mapping uml::Operation::convertOperation: java::Constructor
when {self.name <> self.namespace.name} {
name:= self.name;
}
Although I’ve mentioned mapping
and query
operations, I’ve not made a formal distinction between these two constructs. According to the QVT specification, a query is a special kind of helper
operation. But unlike a query
, a helper
may have side effects on the parameters passed into it.
Queries are intended to simplify expression writing in mapping operations because complex queries are not required to be implemented within an expression. The main restriction on queries is that they cannot create or update object instances, other than for predefined and intermediate types. A query is an operation that has no side effects.
A mapping operation does not return a new instance of the specified model object for a given input instance upon subsequent invocations, based on its trace model. Instead, it returns a reference to the previously mapped instance. With a helper operation, the result is always a new instance.
This query operation returns all objects of type Topic from the elements reference of a Map instance:
query mindmap::Map::getTopics(): Sequence(mindmap::Topic) {
return self.elements->select(oclIsTypeOf(mindmap::Topic))
->collect(oclAsType(mindmap::Topic));
}
In queries, it’s possible to define and assign local variables. For example, the following query returns a dot-delimited fully qualified name for a class based on its package namespace:
query oocore::Class::fullyQualifiedName(): String {
var fqn: String:= self.name;
var pkg: oocore::Package:= self._package;
while (not pkg.oclIsUndefined()) {
fqn:= pkg.name + '.' + fqn;
pkg:= pkg._package;
};
return fqn;
}
Within mapping
and query
operations, objects are created, initialized, passed as parameters, returned as parameters, and more. Although much of the syntax you will use within mapping
and query
bodies is OCL, QVT provides additional features. This section covers essential OCL and QVT operations, mapping
and query
invocation, object creation, and population.
All the common OCL operations and iterators form the basis of QVT, with imperative versions provided to support side effects and strict semantics.
The select()
operation comes from OCL and allows for the filtering of collections to work with a subset. The conditional argument provides for the specification of the filter and can have an optional iterator variable. Following is an example of select
in which all objects of type mindmap::Topic
are returned from the elements collection:
elements->select(oclIsTypeOf(mindmap::Topic))
The select operation has a shorthand notation that uses square brackets, as shown here. This expression is equivalent to the previous one.
elements[oclIsTypeOf(mindmap::Topic)]
The collect()
operation comes from OCL and allows for the creation of one collection from another, typically of different element types. The resulting collection is flattened; collectNested()
and QVT’s xcollect()
can be used when nested collections are required. collect()
is commonly used to gather the elements of a class into a new collection, as illustrated here:
topics->collect(t | t.name);
Here, topics
represents a collection of Topic elements, which have a name attribute of type String. Therefore, the result of this collect()
operation is a collection of Strings representing the name attribute values of each Topic element in topics. This can be written without the iterator variable as simply this:
topics->collect(name);
Collect()
also has a shorthand notation, where a dot (.
) can be used in lieu of the ->collect()
syntax. This makes the previous statement even more simply stated as follows:
topics.name;
It’s common to find select()
and collect()
used together to obtain a collection of commonly typed elements from a reference that can contain multiple subtypes. For example, consider the elements
reference in the Map class that is of type MapElement. Two subtypes exist: Topic and Reference. To obtain just the Topic elements, use the following expression:
var topics: Sequence (Topic):=
self.elements->select(oclIsTypeOf(Topic)->collect(oclAsType(Topic));
-- using shorthand, the following is equivalent:
var topics: Sequence (Topic):=
self.elements[oclIsTypeOf(Topic)].oclAsType(Topic);
QVTO provides a number of imperative operations, including forEach
, forOne
, while
, and switch
. Additionally, imperative versions of OCL are available. This section describes each and provides examples of their use.
The forEach
imperative loop expression executes the loop for all the elements in the collection for which the conditional expression holds. QVTO currently does not support this expression.
The forOne
imperative loop expression executes the loop for only the first element in the collection that satisfies the conditional expression. QVTO currently does not support this expression.
The while
control expression iterates on an expression until its condition is false
. You can terminate a while
using a break, or you can direct execution to the beginning of the next iteration at any point using the continue expression. Following is an example of a while
loop used to create a table name from a class name:
/**
* Replaces camel case with underscore, e.g. firstName -> FIRST_NAME
*/
query String::toColumnName(): String {
var name: String:= '';
var digit: String:= '';
var pos: Integer:= 1;
while (pos <= self.size()) {
digit:= self.substring(pos, pos);
if digit.toLowerCase() <> digit then {
name:= name + '_' + digit;
} else {
name:= name + digit;
} endif ;
pos:= pos + 1;
};
return name.toUpperCase();
}
The switch
imperative expression evaluates condition-based alternatives. It is popular when dealing with enumeration types, as shown in the following example. Note that the more familiar case syntax is also available, in addition to what’s shown here.
query mindmap::Topic::getPriority(): String {
var pri: String:= null;
switch {
(self.priority = Priority::HIGH) ? pri:= 'High';
(self.priority = Priority::MEDIUM) ? pri:= 'Medium';
(self.priority = Priority::LOW) ? pri:= 'Low';
else ? assert fatal (false)
with log('Priority unsupported', self);
};
return pri;
}
In the example, the priority is evaluated against the enumeration literal, with a String returned for each match. If no matches are found, the else
statement invokes a fatal assertion to terminate execution and log the appropriate message.
A set of six imperative iterate expressions are available: xcollect
, collect
,
Onecollect select
, collect selectOne
, xselect
, and selectOne
. Each of these iterates over the source collection to populate the target using iterator variables, a body, and a condition expression. These are similar to their OCL counterparts but can be interrupted using break, continue, raise, and return expressions. Perhaps the most important difference is that null
values are not included in the result set.
The xcollect
imperative iterate expression is similar to its collect
counterpart, but with the important distinction that it does not flatten the result. This makes it more comparable to the OCL collectNested()
operation. This means that xcollect
can return nested collections such as {1, {2, 3}}, which collect
would otherwise return as {1, 2, 3}.
As for the collection types xcollect
operates on and returns, Sets
and Bags
result in a Bag
, while OrderedSets
and Sequences
result in a Sequence. In both cases, duplicates are possible.
You can think of the collect select
imperative iterate expression as a single loop combination of collect and select, where null values are removed from the result. The remaining iterators are self-explanatory.
The type of objects contained within the results of these iterators depends on the use of its conditional, if specified. When a Type is specified as the conditional, it is evaluated using the Boolean oclIsKind(Type)
and returns a sequence casted to the specified Type.
Shorthand notation is available for these imperative expressions, as described in the following examples. Notice that the shorthand is also available to operations, as shown in the last two examples.
-- An example of collect select
self.elements->collect select(i; a=i.name | a.startsWith('A'));
-- An equivalent collectselect using shorthand notation
self.elements->name[a | a.startsWith('A')];
-- An equivalent collectselect shorthand without a target variable
self.elements->name[startsWith('A')];
-- An example of xcollect
self.elements->xcollect(a | a.name);
-- An equivalent xcollect using shorthand notation
self.elements->name;
-- An example of xselect
self.elements->xselect(Topic);
-- An equivalent xselect using shorthand notation
self.elements[Topic];
-- An example of collectselect shorthand with an operation
main () {
inModel.rootObjects()[Map].map toRequirementsModel();
}
-- An example of collectselectOne shorthand using '!'
main () {
inModel.rootObjects()![Map].map toRequirementsModel();
}
As mentioned earlier, a mapping invocation implicitly creates an instance of the declared return type or types. The features of instantiated elements are set using the assignment operator (:=
), as shown earlier with the name:= self.name
statement. Although this works for mappings of simple attributes of the same type, more complex mappings necessarily require more complex expressions. Note that a second assignment operator (+=
) is available for adding to collections.
To create an object within the context of an operation, use the object
keyword. Following is a basic object
expression that creates an instance of PrimitiveDataType
and assigns it to the type reference in the TableColumn
object created by the mapping:
type:= object RDB::datatypes::PrimitiveDataType {
name:= 'int';
};
In fact, the entire body of a mapping can be contained within an object
block, as shown in the complete mapping definition. This is not necessary, however, because the return type of the mapping is enough to determine what the body is instantiating.
mapping UML::Property::primitiveAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isPrimitive() }
{
object RDB::TableColumn {
isPrimaryKey:= self.isPrimaryKey();
name:= self.name;
type:= object RDB::datatypes::PrimitiveDataType {
name:= umlPrimitive2rdbPrimitive(self.type.name);
};
}
}
A common use of object
is to initialize variables in the init section. For example, the following snippet has a variable primitiveType
assigned to an object that was created and initialized using the umlPrimitive2rdbPrimitive()
query, which is later used to set the TableColumn
’s type reference.
mapping UML::Property::primitiveAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isPrimitive() }
{
init {
var primitiveType: RDB::datatypes::PrimitiveDataType:=
object RDB::datatypes::PrimitiveDataType {
name:= umlPrimitive2rdbPrimitive(self.type.name);
};
}
isPrimaryKey:= self.isPrimaryKey();
name:= self.name;
type:= primitiveType;
}
As stated earlier, objects that are created are first checked for existence. If they are null
, a new object of the stated type is instantiated and initialized in the order of the statements in the body. If the object already exists, its contents are updated according to the statements in the body. This implies that update semantics are used in object statements where an object has already been instantiated. Consider this example:
mapping UML::Property::primitiveAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isPrimitive() }
{
init {
result:= object RDB::TableColumn {
isPrimaryKey:= self.isPrimaryKey();
type:= object RDB::datatypes::PrimitiveDataType {
name:= umlPrimitive2rdbPrimitive(self.type.name);
};
};
}
name:= self.name;
}
This example shows the use of the result
keyword, in addition to the inlining of mapping operations. By default, the returned model instance of a mapping is assigned to the result
, which we’re explicitly setting here in the init
section. We’re then updating the result in the mapping body to set the name property. Technically, the implicit instantiation section that exists between the init
section and the population
section (or the mapping body, in this case) recognizes the existence of the instantiated result and incorporates update semantics.
If you examine the output of this mapping, you’ll find that isPrimaryKey
and type
are both set correctly during the init
, while the name of the returned instance is simply updated in the mapping body without instantiating a new TableColumn
. Similarly, access to the result is available in the end{}
block, as shown next. Here again, update semantics are in effect in both the body and end blocks because there is an implicit instantiation of the return object.
mapping UML::Property::primitiveAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isPrimitive() }
{
name:= self.name;
end {
result.isPrimaryKey:= self.isPrimaryKey();
result.type:= object RDB::datatypes::PrimitiveDataType {
name:= umlPrimitive2rdbPrimitive(self.type.name);
};
}
}
Another reason to explicitly create an object is for assignment to return parameters where more than one is defined for a mapping. Consider this example:
mapping X::toYZ(): y:Y, z:Z {
object y:Y {
};
object z:Z {
};
}
When invoking a mapping, use either the map
or xmap
keyword, where xmap
represents invocation with strict semantics. The map
keyword is used after a dot (.) or alternatively after an arrow (->
) when using the collect shorthand. When invocated, as long as the mapping can be resolved using the actual context and the guard conditions are satisfied (if present), the trace is consulted to look for target instances produced from the given source object. If present, the relation holds and the previous result is returned. Otherwise, the mapping body is executed.
In this first example, the input is being mapped using nonstrict semantics to a requirements Model using the more popular form of invocation.
main(in mmap: mindmap::Map@inModel, out req:
requirements::Model@outModel) {
req:= mmap.map toRequirementsModel();
}
mapping mindmap::Map::toRequirementsModel(): requirements::Model {
. . .
}
If we simply change map
to xmap
, we change the invocation semantics to strict. In this case, if the called mapping has a when
clause that is not satisfied, an exception is thrown. Using map
, the mapping simply returns null
.
main(in mmap: mindmap::Map@inModel, out req:
requirements::Model@outModel) {
req:= mmap.xmap toRequirementsModel();
}
Both map
and xmap
can be called using an arrow instead of the dot notation. This implies that the mapping operation is the body of an xcollect
imperative collect construct.
OML provides resolve
, resolveone
, resolveIn
, and resolveoneIn
resolution operators that reference trace data to resolve created objects, or objects used as the source of an object creation, in the case of their inverse variants. These can be useful to update or reference objects created from executed mappings. We cover each of these in turn, along with their late
versions, which are designed to improve transformation efficiency.
The most fundamental resolution operator is resolve
. It returns an object created from a mapping operation. The resolve
operator can take no arguments, a type argument, or a Boolean type condition. Consider an example of each:
source->resolve(); -- select any object
source->resolve(Type); -- select only Type instances
-- select Type instances where the name attribute equals 'aName'
source->
resolve(t:Type | t.name = 'aName'),
The type returned from a resolve operation matches that provided, or a collection (Sequence) of the type. If no type is specified, OclAny
is the result type. Technically, the specification calls for a return type of Object
, but the current QVTO implementation returns its subclass OclAny
.
Let’s consider our mindmap-to-requirements transformation to describe the resolve
functions. As you might recall, a mindmap has a collection of Relationship elements that are mapped to Requirement dependency references when the type of Relationship is of Type::DEPENDENCY
. To create this mapping, we first need to obtain the Requirement object that was created from the source Topic in the Relationship. For this, we can use the following sourceReq
variable assignment:
mapping mindmap::Relationship::toDependency()
when { self.type = mindmap::Type::DEPENDENCY }
{
init {
var sourceReq: requirements::Requirement:=
self.source.resolve(requirements::Requirement)->any(true);
}
...
}
This mapping is invoked from the end
block of our main mapping, so all Topic to Requirement mappings are completed. In this case, the resolve
operation returns all Requirement objects that were created from the Topic object referenced in our source
reference. We know that there is a one-to-one mapping in this case, so we can filter the resulting Sequence
using any(true)
.
Having obtained the Requirement mapped from the source
Topic in our Relationship, we now need to find the Requirement object created from the Topic referenced by the target
reference and add it to our collection of sourceReq
dependencies. We can accomplish this using another resolve
operation. Although resolve
returns all possible mappings, we know that only one will be possible.
sourceReq.dependencies +=
self.target.resolve(requirements::Requirement);
When we’re interested in only the first suitable result when using resolve, we can use the resolveone
alternative. If no suitable results are found, a null
is returned.
Returning to our example, although resolve
worked in the last case, we can improve it a bit by specifying that we want only the first result. So combining the two fragments and replacing resolve
with resolveone
, we have the following equivalent version:
mapping mindmap::Relationship::toDependency()
when { self.type = mindmap::Type::DEPENDENCY }
{
init {
var sourceReq: requirements::Requirement:=
self.source.resolveone (requirements::Requirement);
sourceReq.dependencies +=
self.target.resolveone (requirements::Requirement);
}
}
If we want to further restrict the possible results to objects created by a specific mapping, we can use the resolveIn
and resolveoneIn
variants. These take an additional argument to represent the qualified identifier of the mapping. If multiple mappings have the same name with different signatures, an ambiguity error is reported.
In the case of our mindmap-to-requirements mapping, we have only one mapping that produces Requirement objects from Topic objects, but if there are more in the future, we can restrict those resolved by specifying the current mapping as follows:
var sourceReq: requirements::Requirement:=
self.source.resolveoneIn(mindmap::Topic::toRequirement,
requirements::Requirement);
sourceReq.dependencies +=
self.target.resolveoneIn(mindmap::Topic::toRequirement,
requirements::Requirement);
Sometimes we’re interested in the inverse resolution, or finding the source object that was used in a mapping to create or update an object. By prefixing the resolve operators with inv
, we can achieve the inverse—for example, invresolve
, invresolveone
, invresolveIn
, and so on.
When performing transformations, resolving objects that were not yet created during the execution of a mapping might require more than one pass over a model. To solve this problem, you can modify resolution operators with a late
operator to defer evaluation until the end of the transformation. This technique is always used with assignment operators, where a null
assignment is created until resolution is completed. Use transformation properties instead of local variables so that they are valid when the transformation ends.
Keep in mind that the left side of a late
resolution statement is not re-executed along with the right-side deferred statement. This means that you cannot expect to use the result of the deferred assignment in a later expression. For example, the following assignment of the variable p
is not the result of the resolve
operation; the result is null
because that was assigned during the normal execution.
mapping mindmap::Topic::toRequirement(): requirements::Requirement {
parent:= self.parent.late resolve(requirements::Requirement)
->asSet();
end {
var p:= result.parent->asOrderedSet();
}
}
Furthermore, late
resolutions are invoked sequentially at the end of the transformation in the order they were encountered during normal execution. Don’t rely on the result of a late
resolution that might not have been executed.
As we mentioned earlier, invoking our example mapping from the end
block ensures that we can resolve Requirement objects created from our Topic objects. We can alter this approach using the late
resolution operator, which defers resolution until the end of the transformation and provides the same outcome. Following is an example of this approach:
property dependencies: Set(Relationship) = null;
mapping main(in mmap: mindmap::Map, out req: requirements::Model) {
init {
this.dependencies:= mmap.dependencies();
. . .
}
}
mapping mindmap::Topic::toRequirement(): requirements::Requirement {
title:= self.name;
children += self.subtopics.map toRequirement();
dependencies += this.dependencies
->select(source = self).target.late
resolveIn(mindmap::Topic::toRequirement,
requirements::Requirement);
}
Here we’ve reworked our example a bit to illustrate late resolveIn
. A dependencies property is declared in the transformation and initialized in our main mapping using a query that selects all Relationships of Type::DEPENDENCY
(not shown). When mapping our Topic elements to Requirement elements, we add to the Requirement’s dependencies list those Requirements that were resolved using the toRequirement()
mapping, where the Topic is specified as the target of the Relationship and where the current Topic is the source. During execution, all Topics might not yet have been processed, so late
enables us to avoid a second pass. Section 6.6, “Transforming a Mindmap to Requirements,” fully explains this example.
Note that although it’s legal to combine the inv
variant with a late
operator, it doesn’t make much sense to do so. This is because the source object would always be available using a non-late
resolve operation.
Some things that occur during execution of a QVT script are more clear when we have a debugger, trace model, or log facility. This section discusses the execution facilities that are available in the QVTO runtime. Unfortunately, no debugger is yet available from M2M QVTO.
Execution of a transformation results in one or more target models and a corresponding trace model. The trace model contains a recording of each mapping, including input model element and target model element instance data. The trace model is consulted during execution and for reconciliation during subsequent transformation, to allow for model update semantics.
When an object is created within a transformation, an entry in a trace file is made. When using M2M OML, you’ll find a .qvtotrace
model file created when you execute a transformation, as specified in the launch configuration. Feel free to examine this model to better understand its contents and how it works.
To output information on transformation execution to the environment, a log
expression is provided and has the following syntax. This expression is helpful when debugging QVT scripts or for understanding how they work.
log(message, [object], [level]) [when condition];
Only the first argument is required, but the output also can include a reference to the relevant object and a level. The conditional is also optional and can reference the relevant object. For example:
init {
log('Input map:', mmap, 3) when mmap.elements->size() > 0;
}
Using M2M QVT OML, this outputs to the console the toString()
result of the mmap object, prefixed by Level 3 -
and the String Input map:
, as shown here:
Level 3 - Input map:, data:
org.eclipse.emf.ecore.impl.DynamicEObjectImpl@6fa74a (eClass:
org.eclipse.emf.ecore.impl.EClassImpl@d467a6 (name: Map)
(instanceClassName: null) (abstract: false, interface: false))
The level value has no strict meaning in QVT for log
messages, and M2M QVT OML allows any integer value, leaving it up to the user to specify and interpret levels.
Log output in M2M QVT OML is displayed in the Eclipse Console view. Optionally, the launch configuration can redirect the output to a file, as discussed in Section 6.5.5, “Launch Configuration.”
If a condition needs to be checked during transformation execution, you can use the assert
expression and combine it with the log
expression to output information. You can assign severity to an assertion with the levels warning
, error
, or fatal
, with error
being the default. If a fatal
severity is declared, the transformation execution terminates if the assertion fails. This is the general syntax of the assert
expression:
assert [severity] condition [with log]
For example, the following assertion checks whether a Topic element’s name attribute begins with an underscore and logs a message if it does:
init {
assert warning (not name.startsWith('_')) with
log('Topic name begins with underscore', name);
}
Note that the surrounding parentheses are required per the grammar provided in the spec, although this is not shown in Section 8.2.2, “AssertExp.”
The default output is the severity level prefixed by ASSERT
and followed by failed at <line_number>
, with optional log
output shown here:
ASSERT [warning] failed at (UknownSource:27):
Topic name begins with underscore, data: _A sub-subtopic
You can invoke one transformation from another transformation using the transform()
operation. At this time, transformation composition is not available in QVTO.
An implicitly imported library for all QVT transformations is the Stdlib library. This section covers the types and operations defined in this library. Some operations that are not currently available are not covered here or are indicated as such. For a complete list of standard library features, refer to the QVT specification.
A couple operations are defined for use on Objects.
Object::repr(): String
Returns a String representation of an object, similar to the Java toString()
method. This is handy in log()
statements.
Object::asOrderedTuple(): OrderedTuple(T)
Converts objects not already ordered into an ordered Tuple. This operation is not yet implemented.
In addition to MOF (Ecore) reflective operations, several operations are available on all Elements.
Element::_localId(): String
Returns a local internal identifier. This operation is not currently implemented.
Element::_globalId(): String
Returns a global internal identifier. This operation is not currently implemented.
Element::metaClassName(): String
Returns the name of the metaclass. For example, where self
is of type mindmap::Map
, the output of the following log()
operation is Map
.
log(self.metaClassName());
Element::subobjects(): Set(Element)
Returns all immediate children objects of the Element.
Element::allSubobjects(): Set(Element)
Recursively returns all children objects of the Element.
Element::subobjectsOfType(OclType): Set(Element)
Returns all immediate children objects of the Element that are of the specified type.
Element::allSubobjectsOfType(OclType): Set(Element)
Recursively returns all children objects of the Element that are of the specified type.
Element::subobjectsOfKind(OclType): Set(Element)
Returns all immediate children objects of the Element that are of the specified kind (type plus subtypes).
Element::allSubobjectsOfKind(OclType): Set(Element)
Recursively returns all children objects of the Element that are of the specified kind (type plus subtypes).
Element::clone(): Element
Creates a new instance copy of the model element. The clone is placed in the first model extent. The copy is of only the first-level object, not subobjects. For cloning subobjects as well, see deepclone
.
Element::deepclone(): Element
Creates a new instance copy of the model element, including subobjects.
Element::markedAs(value:String): Boolean
An operation that is defined for each model type. It can determine whether an element is marked, as is the case when accessing an MOF::Tag
. This operation is not currently implemented.
Element::markValue(): Object
An operation used to return the value associated with a marked element. This operation is not currently implemented.
Element::stereotypedBy(String): Boolean
An operation used to determine whether an element is stereotyped. This operation is not currently implemented.
Element::stereotypedStrictlyBy(String): Boolean
An operation similar to stereotypedBy()
, except that the base stereotype is not considered. This operation is not currently implemented.
Model objects that are declared in a transformation signature are available to be accessed throughout the transformation definition. A number of operations are available on these objects and are covered here.
M2M QVT OML provides access to the Ecore features available on the Model objects as well. It’s possible to access a model’s eAnnotations
property or eContainer()
method, for example. For a list of what’s available in Ecore, refer to EMF documentation [38].
Model::objects(): Set(Element)
Returns a list of objects in the model extent, or a flattened set of all objects contained in the passed model instance.
Model::objectsOfType(OclType): Set(Element)
Returns a list of objects from the set of flattened model objects that are of the specified type. Following is an example in which objects of type mindmap::Map
are selected from the input model:
inModel.objectsOfType(mindmap::Map).map toRequirementsModel();
Model::rootObjects(): Set(Element)
Returns a list of objects found at the root of the model—that is, those not contained within any other model object. In the case of typical Ecore models, this is a single model object. In the case of XMI files, there can be multiple root objects. In the case of XSD-based model instances, the root object is the DocumentRoot object.
Model::removeElement (Element):Void
Removes an object from the model, including all links to other objects. This can be useful when cleaning up a model created when intermediate or unwanted objects exist.
Model::asTransformation(Model):Transformation
Casts a model that complies to the QVT metamodel to a transformation instance, for invocation of dynamically defined transformations. This operation is not yet implemented.
Model::copy():Model
Creates a new instance of a model from an existing model, including all objects in the model extent. This operation is not yet implemented.
staticModel::createEmptyModel():Model
Creates and initializes a model of the specified type. This operation is intended for use when creating intermediate models within a transformation. This operation is not yet implemented.
QVT provides a number of list operations, in addition to the collection operations that OCL provides.
List(T)::add(T):Void
Adds an element to the end of a mutable list of this type of element. A synonym operation is append()
. This operation is not yet implemented.
List(T)::prepend(T):Void
Adds an element to the beginning of a mutable list of this type of element. This operation is not yet implemented.
List(T)::insertAt(T, int:Integer):Void
Inserts an element into a mutable list of this type of element at the specified index location. This operation is not yet implemented.
List(T)::joinfields(sep:String, begin:String, end:String):String
Creates a String of list items separated by sep
that is prefixed by begin
and suffixed by end
Strings. This operation is not yet implemented.
Set(T)::asList(): List(T)
OrderedSet(T)::asList(T): List(T)
Sequence(T)::asList(T): List(T)
Bag(T)::asList(T): List(T)
Converts a collection from the specified type into an equivalent mutable List. This operation is not yet implemented.
Only one operation defined in the specification for use on numeric types. Beyond the range()
operation, M2M QVT OML provides additional operations that are covered here.
Integer::range (start:Integer, end:Integer): List(Element)
Returns the list of Integers in the range between the passed start
and end
Integers. This operation is not currently implemented.
Integer::toString():String
Returns a String of the Integer value.
QVT builds upon OCL, so the normal OCL String operations are available within your scripts and are described in the OCL specification. Additionally, the following Strings are available in the standard library.
String::format (value:Object):String
Similar to the Java format()
method and the C printf()
function, this operation prints a message substituting parameters %s (String)
, %d (Integer)
, and %f (Float)
with a value. If multiple parameters are declared, a Tuple is passed as the value, with its elements used for substitution. Additionally, a Dictionary can be used for the value, in which case the format of the parameter is %(key)s
, where key
is looked up in the Dictionary.
This operation is not currently implemented.
String::size ():Integer
Returns the number of characters in the String. A synonym operation length()
is called out in the specification but is not implemented in M2M QVT OML. Following is an example of size()
and its output from within a log expression where toString()
is used to convert the Integer:
log('This string has 29 characters'.size().toString());
result: 29
String::substringBefore (match:String):String
Returns the substring that appears in the character sequence before the sequence to match passed as a parameter. If no match is found, the entire String is returned.
log('test'.substringBefore('s'));
result: te
String::substringAfter (match:String):String
Returns the substring that appears in the character sequence after the sequence to match passed as a parameter. If no match is found, an empty String is returned.
log('test'.substringAfter('s'));
result: t
String::toLower ():String
Returns a String with all characters from the original converted to their lowercase equivalents. For example, the following String outputs what follows to the console:
log('RemovE tHe UpPerCaSe ChAracterS'.toLower());
result: remove the uppercase characters
Note that M2M QVT OML provides a synonym operation toLowerCase()
as well.
String::toUpper ():String
Returns a String with all characters from the original converted to their uppercase equivalents. For example, the following String outputs what follows to the console:
log('RemovE tHe LoWerCaSe ChAracterS'.toLower());
result: REMOVE THE LOWERCASE CHARACTERS
Note that M2M QVT OML provides a synonym operation toUpperCase()
as well.
String::firstToUpper ():String
Converts the first character in a String to its uppercase equivalent, as shown here:
log('test'.firstToUpper());
result: Test
String::lastToUpper ():String
Converts the last character in the String to its lowercase equivalent, as shown here:
log('test'.lastToUpper());
result: test
String::indexOf (match:String):Integer
Returns the index of the first character found in the match String on the target String. If the match is not found, -1
is returned. Following is an example with corresponding log
output.
vars:String:= 'Find me in the string';
log(s.indexOf('me').toString());
result: 5
Note that M2M QVT OML provides another variant of this operation not listed in the specification:
String::indexOf(match: String, startIndex: Integer): Integer
Following is an example of this version of indexOf()
, which provides the index of the first character in the match String after the startIndex
argument position. In this case, the second me
String begins at index 17:
vars:String:= 'Find me and then me in the string';
log(s.indexOf('me', 6).toString());
result: 17
String::endsWith (match:String):Boolean
Returns true
if the String terminates with the match String provided as an argument. Following is an example of an assertion that looks for sentences ending without a period and logs a warning if found:
varsentence:String:= 'I end without a period';
assertwarning (sentence.endsWith('.'))
with log('Sentence ends without a period', sentence);
String::startsWith (match:String):Boolean
Returns true
if the String begins with the match String provided as an argument. Following is an example of an assertion that looks for sentences beginning with whitespace and logs a warning if found:
varsentence:String:= ' I start with a space.';
assertwarning (notsentence.startsWith(' '))
with log('Sentence starts with a space', sentence);
String::trim ():String
Returns a String with leading and trailing whitespace removed. Note that the sentence
variable in the previous startsWith()
example could be corrected using trim()
:
sentence.trim();
String::normalizeSpace():String
This operation goes one step further than the trim()
operation by removing excess whitespace within a String and also removing leading and trailing whitespace. Whitespace sequences are replaced by a single space in the returned String.
log('A sentence with extra spaces '.normalizeSpace());
result: A sentence with extra spaces
String::replace (m1:String, m2:String): String
Returns a String with all occurrences of String m1
replaced with String m2
. Following is an example of package .
(dot) notation replaced with directory path delimiters (/). (Note the escape character before the dot:
varpkg:String:= 'org.eclipse.mindmap';
log('Converted package to path', pkg.replace('.', '/'));
The specification indicates that replace()
will work on all occurrences, but we see here that only the first is replaced:
Converted package to path, data: org/eclipse.mindmap
Eclipse M2M QVT OML provides a replaceAll()
operation that does what we expect:
varpkg:String:= 'org.eclipse.mindmap';
log('Converted package to path', pkg.replaceAll('.', '/'));
Now we get what we wanted in the console output:
Converted package to path, data: org/eclipse/mindmap
If we had not used the escape character in the first example, the output would have been this:
Converted package to path, data: /rg.eclipse.mindmap
String::match (matchpattern:String):Boolean
Returns true
if the regex matchpattern is found in the String. If the pattern is not found, it returns false
. The following example outputs true
.
log('xxxy'.match('x*y').repr());
String::equalsIgnoreCase (match:String):Boolean
Returns true
if the String is the same as the match String, without taking case into account. Returns false
otherwise. The following example outputs true
.
log('a simple test'.equalsIgnoreCase('A Simple Test').repr());
String::find (match:String): Integer
Returns the index of the start of the substring that equals the match String, or -1
otherwise. The following example returns 10
.
log('find the x character'.find('x').repr());
String::rfind (match:String):Integer
Returns the index of the start of the substring beginning from the right that equals the match String, or -1
otherwise. The following example returns 10
.
log('find the x character'.rfind('x').repr());
String::isQuoted (s:String): Boolean
Returns true
if the String begins and ends with the argument String, and returns false
otherwise. The following example returns true
.
log('”is quoted?”'.isQuoted('”').repr());
String::quotify (s:String): String
Returns a String that begins and ends with the argument String. The following example outputs the String “quote me”
.
log('quote me'.quotify('”'));
String::unquotify (s:String):String
Returns a String that has the argument String removed from the beginning and end of the String, if it’s found. Otherwise, it returns the content of the source String. The following example returns the String do not quote me
.
log('“do not quote me”'.unquotify('”'));
String::matchBoolean (s:String):Boolean
This non-case-sensitive operation returns true
if the String is true
, false
, 0
, or 1
. The following example outputs true true
.
log('true'.matchBoolean(true).repr() + ' ' +
'0'.matchBoolean(false).repr());
String::matchInteger (i:Integer):Boolean
Returns true
if the String represents an Integer. The following example outputs true
.
log('0'.matchInteger(0).repr());
String::matchFloat (f:Float):Boolean
Returns true
if the String represents a Float. The following example outputs true
.
log('0.117'.matchFloat(0.117).repr());
String::matchIdentifier(s:String):Boolean
Returns true
if the String represents an alphanumeric word. The following example returns false
.
log('a8s(c'.matchIdentifier('').repr());
String::asBoolean():Boolean
Returns a Boolean value if the String can be interpreted as a Boolean and null
otherwise. The following returns false
.
log('0'.asBoolean().repr());
String::asInteger():Integer
Returns an Integer value if the String can be interpreted as an Integer, and null
otherwise. The following example returns 99
.
log('99'.asInteger().toString());
String::asFloat():Float
Returns a Float value if the String can be interpreted as a Float, and null
otherwise. The following example returns 99.9
.
log('99.9'.asFloat().toString());
String::startStrCounter (s:String):Void
Creates and initializes a counter with the String. When used with the following counterparts, this operation can provide a convenient means to create indexes. The following example outputs 0
, 1
, 0
, 1
for the sequence of counter operations.
var index: String:= 'index';
String.startStrCounter(index);
log(String.getStrCounter(index).toString());
log(String.incrStrCounter(index).toString());
index.restartAllStrCounter();
log(String.getStrCounter(index).toString());
log(String.incrStrCounter(index).toString());
log(index.addSuffixNumber());
String::getStrCounter (s:String):Integer
Returns the current value of the counter associated with the String. See the example in startStrCounter()
for its usage.
String::incrStrCounter (s:String):Integer
Increments the value of the counter associated with the String. See the example in startStrCounter()
for its usage.
String::restartAllStrCounter ():Void
Resets all String counters. See the example in startStrCounter()
for its usage.
String::addSuffixNumber ():String
Appends the current value of the counter associated with the String to the String. This operation can generate unique internal names. See the example in startStrCounter()
for its usage.
The OML language has many of the same syntax features as programming languages such as Java, plus a number of shorthand notations. This section covers these, along with variations from the specification as implemented by M2M QVT OML.
The QVT specification defines three comment styles. Only two are supported:
-- A single line comment to end of the current line
// A single line comment style that is not supported
/*
* A multiple line comment style that is
* similar to Java.
*/
Literal Strings are delimited by either single or double quotation marks, according to the specification. M2M QVT OML supports only single quote marks:
vars1:String:= 'a string';
Literal Strings that fit in multiple lines can be notated as a list of literal Strings. In the specification, they do not require the concatenation operator +
, although M2M QVT OML requires this. For example:
vars:String:= 'This string is split ' +
'across two lines';
Escape sequences are provided and are the same as those found in the Java language. Table 13-1 lists the supported escape sequences.
Repeatedly typing lengthy operation names in a QVT script, such as oclIsKindOf()
, is tiresome. In addition, superfluous text hinders readability. To address this issue, QVT provides a number of shorthand notations.
• The oclIsKindOf()
operation is used frequently, and you can substitute the unary #
operator for it. Typing #Topic
is equivalent to typing oclIsKindOf(Topic)
. This shorthand notation is not currently supported.
• The oclIsTypeOf()
operation is also commonly used, and you can substitute the unary ##
operator for it. Typing ##Topic
is equivalent to typing oclIsTypeOf(Topic)
. This shorthand notation is not currently supported.
• You can substitute the unary *
operator for the stereotypedBy()
operation. Typing *aStereotype
is equivalent to typing stereotypedBy(“aStereotype”)
. The multiplication operator brings up no ambiguity because of the type involved (String vs. Float/Integer). This shorthand notation is not currently supported.
• You can substitute the unary %
operator for the format()
operation. Typing 'the name is %s
' % name
is the equivalent to typing 'the name is %s
'.format(name)
. Again, ambiguity is eliminated because of the type involved. This shorthand notation is not currently supported.
• Instead of using the single equals sign (=
) equality operator, you can use the more familiar double equals sign (==
) for comparison operations. * This shorthand notation is not currently supported.
• You can replace the not-equal operator <>
with the familiar !=
operator. * This shorthand notation is not currently supported.
• You can use the binary operator +
for String concatenation, thereby replacing 'append'.concat('me')
with 'append' + 'me'
. Again, ambiguity is eliminated by the type involved (String vs. Integer/Float).
• When adding to lists, you can replace the add()
operation with the binary operator +=
—for example, allSubtopics += topic.subtopics()
.
* Note that using ==
and !=
shorthand notation requires a directive comment at the top of the source file. This makes the traditional OCL operators =
and <>
illegal within the file. Following is an example directive comment (although it seems that use-contemporary-syntax would be a more fitting name):
-- directive: use-traditional-comparison-syntax
For each of the ocl
-prefixed operations and types available from OCL, QVT provides a synonym operation that drops the ocl
. For example, you can use isKindOf()
in QVT in addition to the traditional oclIsKindOf()
. Note that this support is not yet part of the M2M QVT OML implementation.
Table 13-2 lists synonyms for predefined OCL operations.
The QVT specification uses a simplified UML and relational database metamodel to illustrate the capabilities of QVT. Both a Relations language and OML solution are provided in the spec, with the latter found in Appendix A. The example provided in M2M QVTO is significantly different from the one in the specification, although an updated one is under development as the implementation matures. For example, to implement the specification’s example, M2M QVTO first needs to support intermediate classes.
The OML implementation from the M2M project includes this transformation as a sample project, which is available at New Examples QVT Transformation (Operational) SimpleUML to RDB Transformation Project. The wizard also creates a launch configuration.
The models used in the transformation are installed as part of the QVTO examples feature. To see the structure of the models, you can use the Metamodel Explorer ( Window Show View Other Operational QVT Metamodel Explorer), or you can obtain the Ecore models themselves from the Eclipse CVS repository and render a diagram. Alternatively, placing the cursor on the modeltype
declaration at the top of the Simpleuml_to_Rdb.qvto
file and pressing F3 opens the Metamodel Explorer and selects the model in the tree.
The M2M QVTO example Simple UML model is slightly more complicated than the one in the specification, but it remains simpler than the actual UML metamodel.
The rdb.ecore
model provided in the sample is much more complex than the model used in the specification. The example model includes additional datatypes, views, and constraints subpackages.
The sample project also includes an instance of the Simple UML model to allow for invocation of the script. To run, expand the Run button on the main toolbar and select the SimpleUML to RDB launch configuration. A Simpleuml_to_Rdb.rdb
model appears in the root of the sample project. You can open this and the source pim.simpleuml
model to compare input and output results in the context of the discussion to follow.
The mapping between these two models is straightforward. Although the OML has no graphical notation, the notation used to describe the Relational implementation can be helpful in understanding the mapping. In fact, you could use GMF to implement a similar notation and diagram for QVTO. This exercise is left to you, with the suggestion that you consider contributing the solution to the M2M project.
Let’s begin with the transformation declaration. Both the Simple UML and RDBMS models are declared using modeltype
statements at the top of the file, along with the transformation
declaration itself. Note that there is no strict
qualifier in the modeltype statements, leaving you free to modify the model and reuse this script—that is, as long as you don’t change it so that it breaks the script. Also, no where
clauses restrict our input model from being passed to the transformation.
modeltype UML uses
'http://www.eclipse.org/qvt/1.0.0/Operational/examples/simpleuml';
modeltype RDB uses
'http://www.eclipse.org/qvt/1.0.0/Operational/examples/rdb';
transformation Simpleuml_To_Rdb(in uml: UML, out RDB);
Notice that the sample uses a shorter notation for the uses
clause, compared to the version in the specification, opting to not surround the URI with the model name. Also, the sample uses the URI declared by the models in their EMF registration, which align better with the EMF convention. It would be possible to import each model into the project and add Metamodel Mappings in the project properties and assign the original URI, as shown next. The only other difference is the model names, which, in this case, reflect the names assigned in the Ecore models.
modeltypeUMLusessimpleuml(”omg.qvt-samples.SimpleUML”);
modeltypeRDBMSusesrdb(”omg.qvt-samples.SimpleRDBMS”);
transformationUml2Rdb(insrcModel:UML,outRDBMS);
Looking at the transformation
declaration, because only one transformation is defined in the file, no braces are required to surround its contents. Note also that a name is assigned to the input UML model but not to the RDBMS output model.
The entry point of a QVTO transformation is its main
mapping. As you can see here, the UML::Model
class is passed in as the root object, and the out
parameter instantiates and returns an RDB::Model
object. Both the input and output parameters are given a name to be accessed by within the body of the mapping.
main(inmodel: UML::Model,outrdbModel: RDB::Model) {
rdbModel:= model.mapmodel2RDBModel();
}
In the body, the rdbModel
output is assigned the results of the model2RDBModel()
mapping, which is invoked by appending .map
to the model element, indicating that it will be passed as the input to the mapping.
The model2RDBModel()
mapping is straightforward and could have been folded into the main mapping. The name
of the UML model is mapped to the name
of the RDB model, and the UML::Model
element is passed to the package2schemas
query.
mappingUML::Model::model2RDBModel(): RDB::Model {
name:=self.name;
schemas:=self.package2schemas();
}
The package2schemas()
query returns an OrderedSet
of Schema objects, which are created from the package and its subpackages. In the body of the query, the UML::Package
is mapped to an RDB::Schema
object by the package2schema()
mapping. The result is unioned with a mapping of subpackages, recursively obtained with calls to package2schemas()
for each subpackage.
query UML::Package::package2schemas(): OrderedSet(RDB::Schema) {
self.map package2schema()->asSequence()->
union(self.getSubpackages()->collect(package2schemas()))
->asOrderedSet()
}
Looking at the package2schema()
mapping, you can see our first when
clause in the script. We want to map UML Package elements to RDB Schema elements only if they contain persistent classes. The hasPersistentClasses()
query can determine this; you can see it here along with the isPersistent()
query it uses. As you can see, a class is persistent if it contains a stereotype equal to the String 'persistent'
.
query UML::Package::hasPersistentClasses(): Boolean {
ownedElements->exists(
let c: UML::Class = oclAsType(UML::Class) in
c.oclIsUndefined() implies c.isPersistent())
}
query UML::ModelElement::isPersistent(): Boolean {
stereotype->includes('persistent')
}
The mapping from package to schema involves selecting and collecting all the UML::Class
instances from the package and invoking the persistentClass2table()
mapping.
mapping UML::Package::package2schema(): RDB::Schema
when { self.hasPersistentClasses() }
{
name:= self .name;
elements:= self.ownedElements->select(oclIsKindOf(UML::Class))->
collect(oclAsType(UML::Class).map persistentClass2table())
->asOrderedSet()
}
The persistentClass2table()
mapping appears next. A when
clause eliminates classes that are not persistent using the same query that was used earlier to determine whether a package contained at least one persistent class. The name of the table is mapped from the name of the class. The columns of the table are created by the class2columns()
query and sorted by name. Primary keys are created using the class2primaryKey()
mapping, while foreign keys are created using the class2foreignKeys()
query.
mapping UML::Class::persistentClass2table(): RDB::Table
when { self.isPersistent() }
{
name:= self.name;
columns:= self.class2columns(self)->sortedBy(name);
primaryKey:= self.map class2primaryKey();
foreignKeys:= self.class2foreignKeys();
}
The class2columns()
query combines the results of the dataType2columns()
and generalizations2columns()
for the class passed as a parameter, returning the union as an ordered set.
query UML::Class::class2columns(targetClass: UML::Class):
OrderedSet(RDB::TableColumn) {
self.dataType2columns(targetClass)->
union(self.generalizations2columns(targetClass))
->asOrderedSet()
}
The dataType2columns()
query combines the results of queries that create columns from primitive, enumeration, relationship, and association attributes, rejecting those that are undefined.
query UML::DataType::dataType2columns(in targetType: UML::DataType):
OrderedSet(RDB::TableColumn) {
self.primitiveAttributes2columns(targetType)->
union(self.enumerationAttributes2columns(targetType))->
union(self.relationshipAttributes2columns(targetType))->
union(self.assosiationAttributes2columns(targetType))
->reject(c|c.oclIsUndefined())->asOrderedSet()
}
The generalizations2columns()
query uses the class2columns()
query on the general class, rejects those undefined, and returns an ordered set.
query UML::Class::generalizations2columns(targetClass: UML::Class):
OrderedSet(RDB::TableColumn) {
self.generalizations->collect(g |
g.general.class2columns(targetClass))
->reject(c|c.oclIsUndefined())->asOrderedSet()
}
The primitiveAttributes2columns()
query simply invokes the primitiveAttribute2column
query for each attribute, returning the results as an ordered set.
query UML::DataType::primitiveAttributes2columns(in targetType:
UML::DataType): OrderedSet(RDB::TableColumn) {
self.attributes->collect(a |
a.primitiveAttribute2column(targetType))->asOrderedSet()
}
Primitive types are filtered using the isPrimitive()
query in the when
clause, which checks to see that the targetType
parameter is of type UML::PrimitiveType
. The column’s isPrimaryKey
property is set using the isPrimaryKey()
query, which checks to see that a stereotype is present equal to the String “primaryKey”
. The name of the primitive type passed in is used as the column name, and the type is set to a PrimitiveDataType
object whose name is initialized by the umlPrimitive2rdbPrimitive()
query. This query uses a simple String comparison of basic types.
mapping UML::Property::primitiveAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isPrimitive() }
{
isPrimaryKey:= self.isPrimaryKey();
name:= self.name;
type:= object RDB::datatypes::PrimitiveDataType { name:=
umlPrimitive2rdbPrimitive(self.type.name); };
}
query UML::Property::isPrimitive(): Boolean {
type.oclIsKindOf(UML::PrimitiveType)
}
query UML::Property::isPrimaryKey(): Boolean {
stereotype->includes('primaryKey')
}
query umlPrimitive2rdbPrimitive(in name: String): String {
if name = 'String' then 'varchar' else
if name = 'Boolean' then 'int' else
if name = 'Integer' then 'int' else
name
endif
endif
endif
}
When mapping enumeration attributes to columns, the enumerationAttributes2columns()
query invokes the enumerationAttribute2column()
mapping for each attribute. A when
clause checks that the passed property is an enumeration using the isEnumeration()
query, which checks that its type attribute is of type UML::Enumeration
.
Again, the isPrimaryKey
property is set using the isPrimaryKey()
query, and the name
attributes are directly mapped. The column’s type is set to a new PrimitiveDataType
object initialized with a name of 'int'
.
query UML::DataType::enumerationAttributes2columns(in targetType:
UML::DataType): OrderedSet(RDB::TableColumn) {
self.attributes->collect(map
enumerationAttribute2column(targetType))->asOrderedSet()
}
mapping UML::Property::enumerationAttribute2column(in targetType:
UML::DataType): RDB::TableColumn
when { self.isEnumeration() }
{
isPrimaryKey:= self.isPrimaryKey();
name:= self.name;
type:= object RDB::datatypes::PrimitiveDataType { name:= 'int'; };
}
query UML::Property::isEnumeration(): Boolean {
type.oclIsKindOf(UML::Enumeration)
}
Relationships are mapped to columns using the relationshipAttributes2columns()
query. Unlike the primitive and enumeration types, which map to simple columns, relationships involve the creation of foreign keys. The input targetType
parameter is passed to the relationshipAttribute2foreignKey()
mapping, where a when
clause checks that it is a relationship type using the isRelationship()
query. In this case, valid relationships are data types that are persistent.
Looking closer at our relationshipAttributes2columns()
query, after the collection of ForeignKey
elements, those that are undefined are rejected. From this collection, the TableColumn
objects from each includedColumns
attribute of the foreign keys are collected and returned in an ordered set.
query UML::DataType::relationshipAttributes2columns(in targetType:
UML::DataType): OrderedSet(RDB::TableColumn) {
self.attributes->collect(map
relationshipAttribute2foreignKey(targetType))->reject(a |
a.oclIsUndefined())->
collect(includedColumns)->asOrderedSet();
}
To create a ForeignKey
object from a DataType
that is a Relationship, first the key’s name
is set to the relationship name
prefixed with FK
. The includedColumns
attribute is set using the dataType2primaryKeyColumns
query, which passes a Boolean value equal to the result of the isIdentifying()
query. This query looks for a stereotype String equal to identifying
.
Finally, the referredUC
property is set using a late resolveoneInclass2primaryKey()
mapping. Using late
means the mapping will be invoked at the end of the transformation, which allows for the resolution of objects that might not yet be created. Using resolveoneIn
returns a primary key object that has previously been created by the type attribute of the passed in property. This is accomplished by examining the trace model that is created during the transformation and avoids creating duplicate object instances.
mapping UML::Property::relationshipAttribute2foreignKey(in targetType:
UML::DataType): RDB::constraints::ForeignKey
when { self.isRelationship() }
{
name:= 'FK' + self.name;
includedColumns:=
self.type.asDataType().dataType2primaryKeyColumns(self.name,
self.isIdentifying());
referredUC:= self.type.late
resolveoneIn(UML::Class::class2primaryKey,
RDB::constraints::PrimaryKey);
}
query UML::Property::isRelationship(): Boolean {
type.oclIsKindOf(UML::DataType) and type.isPersistent()
}
To create primary key columns from data types, two parameters are passed in addition to the data type: a prefix
String for the column name and a Boolean to set the isPrimaryKey
property. The body is a little complicated. First the dataType2columns()
query is called, and those that are primary keys are selected. Then a collection based on TableColumn
objects created using passed parameters is returned as an ordered set.
query UML::DataType::dataType2primaryKeyColumns(in prefix: String,
in leaveIsPrimaryKey: Boolean): OrderedSet(RDB::TableColumn) {
self.dataType2columns(self)->select(isPrimaryKey)->
collect(c | object RDB::TableColumn {
name:= prefix + '_' + c.name;
domain:= c.domain;
type:= object RDB::datatypes::PrimitiveDataType {
name:= c.type.name;
};
isPrimaryKey:= leaveIsPrimaryKey
})->asOrderedSet()
}
query UML::Property::isIdentifying(): Boolean {
stereotype->includes('identifying')
}
mapping UML::Class::class2primaryKey(): RDB::constraints::PrimaryKey {
name:= 'PK' + self.name;
includedColumns:=
self.resolveoneIn(UML::Class::persistentClass2table,
RDB::Table).getPrimaryKeyColumns()
}
The final mapping for our data types to columns is the associationAttributes2columns()
query. First, attributes that are persistent association types are selected. Columns that are mapped to columns are collected and returned as an ordered set.
query UML::DataType::assosiationAttributes2columns(targetType:
UML::DataType): OrderedSet(RDB::TableColumn) {
self.attributes->select(isAssosiation())->
collect(type.asDataType().dataType2columns(targetType))
->asOrderedSet()
}
query UML::Property::isAssosiation(): Boolean {
type.oclIsKindOf(UML::DataType) and not type.isPersistent()
}
In this chapter, we explored the QVT Operational Mapping Language in detail, as supported by the current release of the M2M QVTO component. Improved support of the Operational language is expected, along with the introduction of support for the Relations language in subsequent releases. In the next chapter, we take a deeper look at the Xpand template language for model-to-text transformation.
3.15.219.217