Chapter 13. Query/View/Transformation Operational Mapping Language

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.

13.1 Transformation Declaration

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:

image

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:

image

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.

13.1.1 In-Place Transformations

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:

image

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.

13.1.2 Extends and Access

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.

13.1.3 Intermediate Elements

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));

13.1.4 Configuration Properties

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.

13.1.5 Renaming Elements

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.

13.1.6 Predefined Variables

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.

13.1.7 Null

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.

13.2 Libraries

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.

13.3 Mapping Operations

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.

image

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.

image

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.

13.3.1 Mapping Body

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.

Execution Semantics

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.”

return

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.

13.3.2 Entry Operation

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();
}

13.3.3 Inheritance, Merger, and Disjunction

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.

inherits

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().

merges

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();
}

Disjunction

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;
}

13.4 Helper Operations

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;
}

13.5 Implementing Operations

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.

13.5.1 Operations and Iterators

All the common OCL operations and iterators form the basis of QVT, with imperative versions provided to support side effects and strict semantics.

select

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)]

collect

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);

13.5.2 Imperative Operations

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.

forEach

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.

forOne

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.

while

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();
}

switch

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.

13.5.3 Imperative Iterate Expressions

A set of six imperative iterate expressions are available: xcollect, collect
One
, collect 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();
}

13.5.4 Object Creation and Population

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 {
  };
}

13.5.5 Mapping Invocation

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.

13.5.6 Resolution Operators

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.

resolve

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);

resolveone

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);
  }
}

resolveIn and resolveoneIn

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);

inv

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.

late

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.

13.5.7 Executing Transformations

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.

Trace Model

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.

Log

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.”

Assert

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

Transformation Composition

You can invoke one transformation from another transformation using the transform() operation. At this time, transformation composition is not available in QVTO.

13.6 Library Operations

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.

13.6.1 Object Operations

A couple operations are defined for use on Objects.

repr

Object::repr(): String

Returns a String representation of an object, similar to the Java toString() method. This is handy in log() statements.

asOrderedTuple

Object::asOrderedTuple(): OrderedTuple(T)

Converts objects not already ordered into an ordered Tuple. This operation is not yet implemented.

13.6.2 Element Operations

In addition to MOF (Ecore) reflective operations, several operations are available on all Elements.

_localId

Element::_localId(): String

Returns a local internal identifier. This operation is not currently implemented.

_globalId

Element::_globalId(): String

Returns a global internal identifier. This operation is not currently implemented.

metaClassName

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());

subobjects

Element::subobjects(): Set(Element)

Returns all immediate children objects of the Element.

allSubobjects

Element::allSubobjects(): Set(Element)

Recursively returns all children objects of the Element.

subobjectsOfType

Element::subobjectsOfType(OclType): Set(Element)

Returns all immediate children objects of the Element that are of the specified type.

allSubobjectsOfType

Element::allSubobjectsOfType(OclType): Set(Element)

Recursively returns all children objects of the Element that are of the specified type.

subobjectsOfKind

Element::subobjectsOfKind(OclType): Set(Element)

Returns all immediate children objects of the Element that are of the specified kind (type plus subtypes).

allSubobjectsOfKind

Element::allSubobjectsOfKind(OclType): Set(Element)

Recursively returns all children objects of the Element that are of the specified kind (type plus subtypes).

clone

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.

deepclone

Element::deepclone(): Element

Creates a new instance copy of the model element, including subobjects.

markedAs

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.

markValue

Element::markValue(): Object

An operation used to return the value associated with a marked element. This operation is not currently implemented.

stereotypedBy

Element::stereotypedBy(String): Boolean

An operation used to determine whether an element is stereotyped. This operation is not currently implemented.

stereotypedStrictlyBy

Element::stereotypedStrictlyBy(String): Boolean

An operation similar to stereotypedBy(), except that the base stereotype is not considered. This operation is not currently implemented.

13.6.3 Model Operations

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].

objects

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.

objectsOfType

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();

rootObjects

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.

removeElement

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.

asTransformation

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.

copy

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.

createEmptyModel

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.

13.6.4 List Operations

QVT provides a number of list operations, in addition to the collection operations that OCL provides.

add

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.

prepend

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.

insertAt

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.

joinfields

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.

asList

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.

13.6.5 Numeric Type Operations

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.

range

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.

toString

Integer::toString():String

Returns a String of the Integer value.

13.6.6 String Operations

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.

format

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.

size

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

substringBefore

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

substringAfter

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

toLower

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.

toUpper

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.

firstToUpper

String::firstToUpper ():String

Converts the first character in a String to its uppercase equivalent, as shown here:

log('test'.firstToUpper());
result: Test

lastToUpper

String::lastToUpper ():String

Converts the last character in the String to its lowercase equivalent, as shown here:

log('test'.lastToUpper());
result: test

indexOf

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

endsWith

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);

startsWith

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);

trim

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();

normalizeSpace

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

replace

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

match

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());

equalsIgnoreCase

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());

find

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());

rfind

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());

isQuoted

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());

quotify

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('”'));

unquotify

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('”'));

matchBoolean

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());

matchInteger

String::matchInteger (i:Integer):Boolean

Returns true if the String represents an Integer. The following example outputs true.

log('0'.matchInteger(0).repr());

matchFloat

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());

matchIdentifier

String::matchIdentifier(s:String):Boolean

Returns true if the String represents an alphanumeric word. The following example returns false.

log('a8s(c'.matchIdentifier('').repr());

asBoolean

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());

asInteger

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());

asFloat

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());

startStrCounter

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());

getStrCounter

String::getStrCounter (s:String):Integer

Returns the current value of the counter associated with the String. See the example in startStrCounter() for its usage.

incrStrCounter

String::incrStrCounter (s:String):Integer

Increments the value of the counter associated with the String. See the example in startStrCounter() for its usage.

restartAllStrCounter

String::restartAllStrCounter ():Void

Resets all String counters. See the example in startStrCounter() for its usage.

addSuffixNumber

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.

13.7 Syntax Notes

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.

13.7.1 Comments

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.
*/

13.7.2 Strings

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.

Table 13-1 QVT Escape Sequences

Image

13.7.3 Shorthand

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

13.7.4 OCL Synonyms

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.

Table 13-2 OCL Operators

Image

13.8 Simple UML to RDBMS Example

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 image Examples image QVT Transformation (Operational) image 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 image Show View image Other image Operational QVT image 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()
}

13.9 Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.217