Skip to main content

VIATRA is an open source model transformation framework, focusing on efficient evaluation of model queries and supports various transformation workflows. This document contains a tutorial for getting started with the query and transformation development.

The tutorial relies on the CPS Demonstrator application. The CPS Demonstrator is specified to cover a usual workflow in Model-driven Engineering, where a system is (1) first described in a source model, then (2) automated model-to-model transformations are used to derive a target model. Finally, (3) model-to-text transformation is performed to generate code from the target domain. In addition, a model generator that can automatically create source domain models can support the correctness testing and performance evaluation of the components. This tutorial uses only a subset of the transformation problem, as its main goal is to illustrate the basics of the VIATRA environment. Consult the original demonstrator for a more complex transformation example.

We expect readers to be familiar with the basics of Eclipse plug-in development, more specifically creating plug-in projects and defining basic UI extensions such as commands. Additionally, we expect a basic understanding of EMF-based modeling. If required, look at the Plug-in development tutorial at http://www.vogella.com/tutorials/EclipsePlugin/article.html or the EMF tutorial at http://www.vogella.com/tutorials/EclipseEMF/article.html for a short presentation on these subjects.

This tutorial includes a quick setup guide, then describes both model query development and their usage in the query runtime. Then, both batch and event-driven transformations are covered.

This document is intended only as a short tutorial for the usage of the Query and Transformation capabilities of VIATRA. A more detailed documentation is maintained at http://wiki.eclipse.org/VIATRA.

1. Setting up the tutorial

This tutorial was written for version 1.6 (released on 28th June 2017).

Starting with version 1.6 VIATRA is available from the Simultaneous Release Train (Oxygen version: http://download.eclipse.org/releases/oxygen); for older releases or for older version use the release p2 repository at http://download.eclipse.org/viatra/updates/release/

Install window
Figure 1. Install window

For a faster installation, you may deselect the Contact all update sites during install... field, but then it might be necessary to install the required Xtext Runtime manually.

This document assumes that the CPS metamodels are installed into the Eclipse instance. They are available from the p2 repository https://hudson.eclipse.org/viatra/job/viatra-examples-cps/lastSuccessfulBuild/artifact/cps/releng/org.eclipse.viatra.examples.cps.update/target/repository/

VIATRA defines a custom perspective called Transformation Development that includes a few views and shortcuts that make it easier to develop queries and transformations. The remainder of the tutorial expects that this perspective is selected; in case a different perspective is used, the corresponding views and wizards are still available in their standard respective locations.

2. Domains

2.1. Cyber Physical System

The CPS domain specifies application and host types and their instances, requests and requirements on applications and resource requirements of applications towards hosts. Application types have a state machine that describes their behavior through states and transitions. Finally, application instances can be allocated to host instances that can communicate with each other.

Cyber Physical System model Ecore diagram
  • Host instances have a unique node IP address.

  • Application instances have a unique identifier.

  • State machines can define an initial state.

  • Transitions may specify an action for sending or waiting for a signal. A signal can be sent by providing an application type (with its ID) and a signal identifier, while waiting for a signal is done by specifying its identifier.

2.2. Deployment

In the deployment model, host instances contain the applications that are running on them, while each application has a behavior with states and transitions. The behavior has a current state and transitions may trigger other transitions when the triggered transition is waiting for the signal they are sending and the application type is correct.

Deployment model Ecore diagram

2.3. Traceability

The traceability model describes the correspondence between a CPS and a deployment model. The traceability is stored in a set of traces that refer to zero, one or multiple CPS and deployment elements.

Traceability model Ecore diagram

3. Query Development

3.1. Using the Query Development Environment

Constraints and conditions in the VIATRA framework are expressed using a graph pattern-based language. This declarative formalism allows very compact definitions of complex conditions, while it is still possible to provide live query evaluation based on the Rete algorithm.

In the following we get an overview of the query development environment, starting with the definition of queries, followed by query evaluation support. Then we gain an understanding of the various language elements by creating more and more complex queries.

A graph pattern encodes a named query with some parameters defined as a disjunction of pattern bodies, while each body consists of a set of constraints. The result of a graph pattern, called match set, is a set of (model element) tuples where the elements fulfill all constraints defined in at least one of the pattern bodies.

The pattern language always works on sets: neither the constraints nor the match set is ordered; and match set never includes multiple tuples with exactly the same model elements.

3.1.1. Define your First Query

To define queries, first a VIATRA Query Project has to be created with the standard New Project Wizard of Eclipse. Such projects are specialized Eclipse plug-in projects with preconfigured VIATRA dependencies and the query generator initialized. Query specifications have to be added to the Java classpath of the projects, more specifically into Java packages. Based on these observations the creation of the first query consists of the following steps:

  1. Create a new VIATRA Query project in the host Eclipse with the following name: org.eclipse.viatra.examples.cps.queries.

  2. Add org.eclipse.viatra.examples.cps.model to the Plug-in dependencies

  3. Create a new query definition in a package named org.eclipse.viatra.examples.cps.queries and a file named queries.vql. In the wizard create an empty query. Fill the first query:

    queries.vql
    // Java package declaration, must match the container of the file
    package org.eclipse.viatra.examples.cps.queries
    
    // EPackage import
    import "http://org.eclipse.viatra/model/cps"
    
    // Pattern declaration
    pattern hostIpAddress(host: HostInstance, ip : java String) {
        // Type constraint stating that variables 'host' and 'ip' are connected via a 'nodeIp' attribute
        HostInstance.nodeIp(host,ip);
    }

Looking at the pattern header, we see that this pattern has two parameters, meaning its results will be a pair of values, the first selecting a HostInstance from the model, while the second one a String literal. The connection between these elements is described by a single constraint ensuring that ip variable stores the nodeIp attribute of a corresponding HostInstance.

3.1.2. Evaluate Queries in the Query Results View

VIATRA includes a view to evaluate the results of queries over various editors, and reacts on changes in the editor.

The Query Results view is the primary tool for debugging graph patterns. Open the view by selecting Window/Show View/Query Results or you can simply press the CTRL+3 shortcut and start to type the name of the view. The view allows loading models and queries, and display (and update) the results of queries automatically. Together with the installed metamodels there is also an example instance model included that will be used in this tutorial as an example.

Generate CPS example
Figure 2. Initialize the example CPS demonstrator project
  1. Open our example instance model (/org.eclipse.viatra.examples.cps.instances/example.cyberphysicalsystem)

  2. Make sure "ReteEngine" is selected in the toolbar of the Query Results view

  3. then press the 'Load model from active editor' (first button on the toolbar)

  4. Open the query specification (vql file)

  5. then press the 'Load queries from the active editor' button

At this point the Query Results view should contain the matches of the freshly created pattern. Using the example model, you can see that there are 6 matches for the pattern, each consisting of a HostInstance-IP address pair. Note that the Query Results view provides live results: by updating the model in the model editor file, e.g. adding a new host instance or changing its IP address, the results update automatically.

Query Results View in Action
Figure 3. Query Results View
If the 'Load model from active editor' button is not enabled, it either means, the current editor does not contain a model, or VIATRA does not understand the editor type. By default, EMF tree editors are supported; other editor types, such as graphical editors are supported by additional integration plug-ins, such as the GMF or Graphiti integration available from the VIATRA repository.

3.1.3. Define Additional Queries

In the following, we define a set of patterns that illustrate additional capabilities of the query language. Each pattern will come with a short definition, followed by the code itself and some remarks about how the pattern works.

  1. List all HostInstance elements whose IP address is an empty string

    • This pattern, similar to the first pattern, still consists of a single constraint. The pattern constraints can refer to Java literals, like empty strings or numbers directly. This pattern should have no matches in the example model, as by default all instances have a non-empty IP address set up.

    • Notice that if you create a new HostInstance element, it will not appear in the match results. This happens because in EMF unset an empty attributes are different. You can write a pattern that finds missing attribute values using the neg find construct (see later).

      pattern emptyIpAddress(host: HostInstance) {
          HostInstance.nodeIp(host, "");
      }
  2. List all HostInstance-HostInstance pairs that share a common IP address

    • This pattern is more complex, as it has three parameters and three constraints. The first two describe similar type constraints we have seen. The pattern also compares the values of variables host1 and host2 with each other using the != (not equal) operator (The == operator is also available).

      pattern sameIpAddress(host1 : HostInstance, host2 : HostInstance, commonIp : java String) {
          HostInstance.nodeIp(host1, commonIp);
          HostInstance.nodeIp(host2, commonIp);
          host1!=host2;
      }
  3. List all HostInstance elements that have non well-formed IPv4 addresses (e.g. not four numbers separated with dots)

    • The well-formedness validation of the IP address strings requires specific validation blocks called check expressions where you can write a wide range of Xbase expressions, behaaving similarly to Java and accessing Java classes from the classpath of the project. In this case, the well-formedness of the address values are inde.

    • It is important to note that check expressions have to be side-effect free and can only be called on attribute variables.

      pattern ipFormatInvalid(host : HostInstance, ip : java String) {
          HostInstance.nodeIp(host,ip);
          check (
              !ip.matches("^[\\d\\.]+")
          );
      }
  4. List State elements connected through Transition elements

    • A pattern body might use variables other than the pattern parameters, such as the variable transition in this example. These variables are called local variables.

    • It is important to note that if there are multiple transitions between two states, the match set will still include only a single pair of the states, because local variables are not included in the match tuples. If all edges are required, the corresponding transition variable should also be declared as a parameter.

      pattern connectedTo(state: State, other: State){
          // There exists a transition from `state` to `other`
          State.outgoingTransitions(state, transition);
          Transition.targetState(transition, other);
      }
  5. List bad host instances that fail either of the previous conditions.

    • Disjunctions can be expressed by using the or keyword between pattern bodies. A model element tuple is included in the match set of a pattern, if at least one of the bodies have a match. Note that if multiple bodies would match the same tuple, the match set of the pattern will still only include the tuple once (set semantics).

    • Patterns can be reused using find constraints meaning all conditions expressed by the called pattern must be matched from the source.

    • This pattern also includes single-use (or don’t care) variables, starting with the character '_'. Such a declaration describes a variable where we are only interested in its existence but not its value.

      pattern badHost(host : HostInstance, ip : java String) {
          find sameIpAddress(host, _other, ip);
      } or {
          HostInstance.nodeIp(host, ip);
          find emptyIpAddress(host);
      } or {
          HostInstance.nodeIp(host, ip);
          find ipFormatInvalid(host);
      }
  6. List all good host instances (that meet neither of the incorrect conditions)

    • The negative pattern composition, expressed by the neg find keyword is used to define negative conditions. This works similar to the find constraints, with the notable exception that if there are any matches to the badHost with the selected parameters, the host pattern fails to match.

    • Those actual parameters of the negative pattern call that are not used elsewhere in the calling body are universally quantified, meaning that the calling pattern only matches if variables of the calling pattern cannot be bound to matching elements.

      pattern goodHost(host : HostInstance, ip : java String) {
          HostInstance.nodeIp(host, ip);
          neg find badHost(host, _);
      }
  7. List the number of applications for each HostInstance

    • Patterns can be marked as private, making the pattern itself only visible inside the source file it is defined. The generated code for these patterns is reduced (e.g. does not include generated Match and Matcher classes for easier access).

    • It is possible to calculate the matches of a pattern using the count find expressions. The value of such an expression is the number of matches found with the selected number of matches.

      private pattern applications(host, app) {
          HostInstance.applications(host, app);
      }
      
      pattern countApplications(host : HostInstance, M) {
          M == count find applications(host, _);
      }
  8. List all states of a state machine that are reachable from its initial state (either directly or indirectly)

    • The reachable states are calculated using the transitive closure of the previously introduced connectedTo pattern.

      pattern reachableState(sm :StateMachine, state: State){
          // The initial state of the statemachine is reachable
          StateMachine.initial(sm, state);
      } or {
          StateMachine.initial(sm, initial);
          StateMachine.states(sm, state);
          // The + symbol after the pattern name represents transitive closure
          find connectedTo+(initial, state);
      }

3.1.4. Validation

VIATRA provides facilities to create validation rules based on the pattern language of the framework. These rules can be evaluated on various EMF instance models and upon violations of constraints, markers are automatically created in the Eclipse Problems View.

The @Constraint annotation can be used to mark a pattern as a validation rule. If the framework finds at least one pattern with such annotation.

Annotation parameters:

  • key: The list of paremeters which determine which objects the constraint violation needs to be attached to.

  • message: The message to display when the constraint violation is found. The message may refer the parameter variables between $ symbols, or their EMF features, such as in $Param1.name$.

  • severity: "warning" or "error"

  • targetEditorId: An Eclipse editor ID where the validation framework should register itself to the context menu. Use "*" as a wildcard if the constraint should be used always when validation is started.

To find a specific editor id, we can use the Plug-in Selection Spy tool with a Shift+Alt+F1 shortcut.

For example:

@Constraint(targetEditorId = "org.eclipse.viatra.examples.cps.cyberPhysicalSystem.presentation.CyberPhysicalSystemEditorID",
            severity = "error",
            message = "The ip address is not unique",
            key = {"host1"})
pattern sameIpAddress(host1: HostInstance, host2: HostInstance, commonIp) {
    HostInstance.nodeIp(host1, commonIp);
    HostInstance.nodeIp(host2, commonIp);
    host1!=host2;
}

3.2. Using Queries Programmatically

VIATRA Query provides an API to execute queries on various models, including support for listening to match set changes. However, as the incremental evaluation relies on indexes, the API also covers lifecycle management for the runtime. The central element of the API is the Query Engine that is responsible for loading query specifications, setting up indexes and providing match results. This approach is supported by code generators that create a runtime representation for graph patterns and provide a type-safe API to access VIATRA code.

query runtime
Figure 4. Overview of the runtime components of VIATRA Query

To start working with the VIATRA Query API, we have to provide (1) a Scope representing the model and a (2) set of query specifications. The easiest way to initialize a scope, is to simply wrap an EMF ResourceSet inside a new EMFScope instace. For query specifications, the generated matcher classes can be used as an example, see as follows.

In general, the generated code in a VIATRA Query project consists of a (1) query specification classes that represents the original VQL specifications for the runtime API, (2) a Match and Matcher class for each pattern definition (recommended for general usage) and (3) a group class for each file that can be used to initialize all queries together.

query generated code
Figure 5. Structure of the query generated code

3.2.1. Initialize a headless Application

To illustrate the usage of the VIATRA Query API, we will create a headless Eclipse application, and execute it over one of the queries written in the previous part. Such an application is a Java class registered using the extension point org.eclipse.core.runtime.applications (requiring the org.eclipse.core.runtime bundle as a dependency).

  <extension id="queryrunner" point="org.eclipse.core.runtime.applications">
    <application cardinality="singleton-global" thread="main" visible="true">
      <run class="org.eclipse.viatra.examples.cps.queries.runner.QueryRunner"/>
    </application>
  </extension>

The IApplication interface requires two methods to be implemented, called start and stop. In our case, we will only use start (and returning 0 to mark successful execution), stop is unnecessary.

public class QueryRunner implements IApplication {

	@Override
	public Object start(IApplicationContext context) throws Exception {
        // Return value 0 is considered as a successful execution on Unix systems
		return 0;
	}

	@Override
	public void stop() {
        // Headless applications do not require specific stop steps
	}

}

The created application can be started as an Eclipse application by specifically selecting the previously created extension.

run headless application
Figure 6. Running the Query Runner application

3.2.2. Initializing a Query Engine

To initialize a query engine, as first step an EMF scope has to be loaded. This can be done using the following code segment (expecting the model file was copied into the root of the queries project):

private EMFScope initializeModelScope() throws IOException, ViatraQueryException {
	ResourceSet rs = new ResourceSetImpl();
	Resource res = rs.createResource(URI.createPlatformPluginURI("org.eclipse.viatra.examples.cps.queries/example.cyberphysicalsystem", false));
	res.load(new HashMap<>());
    return new EMFScope(rs);
}

If we have a model scope, it can be used to initialize a managed query engine. The internal implementation of the ViatraQueryEngine.on method ensure that only a single query engine will be created for each scope, and the query engine will be disposed together with the backing model, making this the preferred implementation for common cases.

Additionally, it is recommended to prepare the engine with all queries that will be used. For this, the generated query groups (one per query file) include a prepare method that creates all indexes required for the pattern matchers, with only a single round of model traversal required.

private ViatraQueryEngine prepareQueryEngine(EMFScope scope) throws ViatraQueryException {
	// Access managed query engine
    ViatraQueryEngine engine = ViatraQueryEngine.on(scope);

    // Initialize all queries on engine
	CPSQueries.instance().prepare(engine);

	return engine;
}
If multiple query groups are to be loaded, either create a generic pattern group that holds all the patterns, or create a coalesce traversal block where you can execute multiple prepare statements together using the engine.getBaseIndex().coalesceTraversals() method.

3.2.3. The pattern matcher API

The easiest way to use all the query engine is to ask for all matches of a query. The getAllMatches method of a pattern matcher returns a set of match objects that allow named references to its parameters

private void printAllMatches(ViatraQueryEngine engine) throws ViatraQueryException {
    // Access pattern matcher
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    // Get and iterate over all matches
    for (HostIpAddressMatch match : matcher.getAllMatches()) {
        // Print all the matches to the standard output
        System.out.println(match.getHost());
    }
}
It is safe to ask for the same matcher multiple times using the on method. Although the returned matcher instances may be different, but internally they reuse the same indexes. Given the matchers themselves are stateless, they are safe to use and forget, and at a later point ask for it again.

It is also possible to use a more functional style processing of matches using match processors.

private void printAllMatches2(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    matcher.forEachMatch(new HostIpAddressProcessor() {

        @Override
        public void process(HostInstance pHost, String pIp) {
            System.out.println(pHost);
        }
    });
}

When using Java 8, the same functionality can be accessed using lambda functions, however in this case only the match can be used as a parameter.

private void printAllMatches3(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    // The lambda method implements IMatchProcessor<HostIpAddressMatch>
    matcher.forEachMatch((match) -> {
            System.out.println(match.getHost());
        });
}

Often it is beneficial to check for and process only a single match. For this reason it is possible to ask for a single match using the getOneArbitraryMatch method.

private void printOneMatch(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    System.out.println(matcher.getOneArbitraryMatch());
}
The match returned by the getOneArbitraryMatch is neither random nor deterministic, but unspecified. Usually repeatedly calling it on the same model (without any model updates) returns the same match, but this is also not guaranteed. On the other hand, restarting the application on the same model usually changes the match returned.

The generated matchers also include a few methods to access values of the parameters. For example, in case of the hostIpAddress pattern there is a getAllValuesOfip method that returns all values the parameter ip finds.

private void printAllAddresses(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    for (String ip : matcher.getAllValuesOfip()) {
        System.out.println(ip);
    }
}
If there are multiple hosts that have the same IP address, the getAllValuesOfip() call will return each IP address only once. This is consistent with all other APIs that always return sets. If duplicates are required, you have to process all matches manually.

All matcher methods allow to filter the matches with constants. By setting some filter parameters with a non-null value we state that we are interesting in only matches

private void printFilteredMatches(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    for (HostIpAddressMatch match : matcher.getAllMatches(null, "152.66.102.2")) {
        System.out.println(match.prettyPrint());
    }
}
Regardless of input values receiving null values, the query engine will never return matches with null values. If no matches fulfill all the set parameters, the returned set will be empty.

If a filter condition has to be reused, it is possible to create mutable matches where the filtered values are set accordingly. This approach is also useful to use named setters (e.g. if multiple String parameters are to be set) or one does not want to write null literals.

private void printFilteredMatches2(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    HostIpAddressMatch filterMatch = HostIpAddressMatch.newEmptyMatch();
	filterMatch.setIp("152.66.102.3");
    for (HostIpAddressMatch match : matcher.getAllMatches(filterMatch)) {
        System.out.println(match.prettyPrint());
    }
}

Finally, if we are only interested in whether there exist any match fulfilling the query, or we want to know how many matches there are, the matcher has methods that calculate these. Both of these methods can be combined with filter matches.

private void countMatches(ViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);
    System.out.printf("Count matches: %d %n", matcher.countMatches());
    System.out.printf("Has matches: %b %n", matcher.hasMatch(HostIpAddressMatch.newEmptyMatch()));
    System.out.printf("Count matches with ip 152.66.102.3: %d %n", matcher.countMatches(null, "152.66.102.3"));
    System.out.printf("Has matches with ip 152.66.102.13: %b %n", matcher.hasMatch(null, "152.66.102.13"));
}
If asking for the has/count calls is followed by the processing of the said matches, it is usually better to call getAllMatches or getOneArbitraryMatch directly, and calculate the count/existence using them.

3.2.4. Advanced query engine features

There are cases where the standard engine lifecycle is inappropriate, e.g. the models will not be unloaded but we want to spare memory by freeing up indexes. Furthermore, there are some functionality, like hint handling or match update listener support that was not added the the base implementation to keep its API clean.

private AdvancedViatraQueryEngine prepareAdvancedQueryEngine(EMFScope scope) throws ViatraQueryException {
    AdvancedViatraQueryEngine engine = AdvancedViatraQueryEngine.createUnmanagedEngine(scope);

    // Initialize all queries on engine
    CPSQueries.instance().prepare(engine);

    return engine;
}
Do not forget to dispose unmanaged engine manually using the AdvancedQueryEngine.dispose() method. If you want to use managed query engines but use the advanced features, you might use the AdvancedQueryEngine.from(engine) call; however, do NOT dispose such engines.
3.2.4.1. React to match updates

One feature of the advanced query engine is to allow listening to changes, e.g. registering a match update listener for a pattern matcher. Such a listener is triggered when the match set for a pattern matcher changes, together with the direction of the changes.

IMatchUpdateListener<HostIpAddressMatch> listener = new IMatchUpdateListener<HostIpAddressMatch>() {

    @Override
    public void notifyAppearance(HostIpAddressMatch match) {
        System.out.printf("[ADD] %s %n", match.prettyPrint());
    }

    @Override
    public void notifyDisappearance(HostIpAddressMatch match) {
        System.out.printf("[REM] %s %n", match.prettyPrint());

    }
};

private void addChangeListener(AdvancedViatraQueryEngine engine) throws ViatraQueryException {
    HostIpAddressMatcher matcher = HostIpAddressMatcher.on(engine);

    try {
        // fireNow = true parameter means all current matches are sent to the listener
        engine.addMatchUpdateListener(matcher, listener, true);
        // execute model manipulations
        matcher.getOneArbitraryMatch().getHost().setNodeIp("123.123.123.123");
    } finally {
        // Don't forget to remove listeners if not required anymore
        engine.removeMatchUpdateListener(matcher, listener);
    }
}
By registering the match update listener with a true value for the fireNow parameter, we ensure that all existing matches are sent to the listener. If we only want to consider future updates, set that parameter to false.

When looking at the output, the setNodeIp call will result in two changes: the first one represents the removal of the old match (host - old IP pair), while the second one represents an addition of a new one (host - new IP pair). In general, model updates often are often represented by two changes.

Be very careful when using match update listeners, as sometimes they are called while the model indexes are in an inconsistent state. For this reason, do not update the underlying model and do not execute further model queries. If such cases are required, delay the execution for a later phase. Better still, you can rely on the transformation API of VIATRA that ensure that rules are only executed when the indexes are in a consistent state.
3.2.4.2. Local Search backends and hints

The advanced query engine also allows to initialize patterns with non-default settings called hints. The most important feature of these hints allow setting the pattern matcher backend, and other backend-specific settings could be changed.

In addition to Rete-based incremental query evaluation VIATRA also includes a local search-based approach. By default, Rete is used, but by adding the org.eclipse.viatra.query.runtime.localsearch bundle as a dependency of the project, it is possible to set up query evaluation hints.

private void queryWithLocalSearch(AdvancedViatraQueryEngine engine) throws ViatraQueryException {
    QueryEvaluationHint hint = LocalSearchHints.getDefault().build();
    HostIpAddressMatcher matcher = engine.getMatcher(HostIpAddressQuerySpecification.instance(), hint);

    for (HostIpAddressMatch match : matcher.getAllMatches()) {
        System.out.println(match.prettyPrint());
    }
}

As you can see, after the initialization the local search based backend can be queried with the same backend as the Rete-based one, however, it calculates the results when queried instead of relying on previously cached results. This means, usually it is cheaper (in memory and prepare time) to initialize a local search based matcher, but gathering the results is more expensive.

As the name suggests, hints might be ignored by the query engine, e.g. if an incorrect configuration was set, or the engine knows of a functionally equivalent way that has a better performance. For details about the hints, consult the LocalSearchHints and ReteHintOptions classes.

There are a few aspects where the current local search backend behaves differently to the original, Rete-based algorithm:

  • Recursive queries are not supported. Trying to initialize a query with recursion results in a runtime error.

  • The algorithm cannot provide change notifications, so registering a MatchUpdateListener over local search-based queries is prohibited.

The local search backend of VIATRA is almost functionally compatible with the Rete-based backend, but has very different performance characterics. If performance is critical, make sure to understand both algorithms to choose the appropriate one for the problem at hand.

4. Model Transformation Development

For model transformation development a Java API is available, allowing seamless integration of the transformations into any Java applications. However, to enhance readability, we recommend using a higher-level JVM language, as it allows defining the transformation as an internal DSL of this host language.

In this tutorial we rely on the Xtend language to host the VIATRA transformation DSL, and we rely on its extension method and type inference support to reduce unnecessary elements. However, other JVM-based languages can also be used with similar efficiency (for an example in Kotlin see https://gist.github.com/doczir/bfe95c470599c5b8e60b400b80f92ea2).

4.1. Batch Transformations

This exercise helps the audience to create a simple batch transformation using the VIATRA Transformation API. The transformation will transform the hosts and applications in a CPS model to a deployment model. The exercise also covers registering a menu command which initializes the transformation.

4.1.1. Create transformation

For the transformation, we have to created a VIATRA Query Project (the one from the query development tutorial could also be reused), and create a new query file called CpsXformM2M.vql to store the patterns we want to use in the transformation with the following contents:

import "http://org.eclipse.viatra/model/cps"
import "http://org.eclipse.viatra/model/deployment"
import "http://org.eclipse.viatra/model/cps-traceability"

pattern hostInstance(hostInstance : HostInstance) {
    HostInstance(hostInstance);
}

pattern applicationInstance(
    appType : ApplicationType,
    appInstance : ApplicationInstance
) {
    HostInstance.applications(_, appInstance);
    ApplicationType.instances(appType, appInstance);
}

/**
 * Traceability link access
 */
pattern cps2depTrace(
    cps2dep : CPSToDeployment,
    trace : CPS2DeploymentTrace,
    cpsElement : Identifiable,
    depElement : DeploymentElement
) {
    CPSToDeployment.traces(cps2dep, trace);
    CPS2DeploymentTrace.cpsElements(trace, cpsElement);
    CPS2DeploymentTrace.deploymentElements(trace, depElement);
}
  • Create transformation class in Xtend

    • Create new Model Transformation with the wizard

      transformation wizard
      Figure 7. Model Transformation Wizard - Create new transformation
    • Setup the name of the transformation and click Next

      batch transformation wizard name
      Figure 8. Model Transformation Wizard - Name of the new batch transformation
    • Setup the type of the transformation to BatchTransformation and click Finish

      batch transformation wizard type
      Figure 9. Model Transformation Wizard - Type of the new transformation

In the created file we have to register a few extension methods, more specifically for our used queries (CpsXformM2m, the same name the VQL file uses) and the EMF EPackages we want to refer (here the deployment and traceability packages). A few additional extension methods are already registered, e.g. transformation rule builder and model manipulation API.

/** VIATRA Query Pattern group **/
val extension CpsXformM2M cpsXformM2M = CpsXformM2M.instance

/** EMF metamodels **/
val extension DeploymentPackage depPackage = DeploymentPackage.eINSTANCE
val extension TraceabilityPackage trPackage = TraceabilityPackage.eINSTANCE
  • Constructor will also initialize transformation (replace the generated one)

    • It assumes that the resource and trace models are already created

    • The IModelManipulations implementation is used to make model access replaceable, this way the same transformation may be used for cases where the resource set is transactional. The initialization of this is generated automatically into the createTransformation method.

      val CPSToDeployment cps2dep
      
      new(CPSToDeployment cps2dep, ViatraQueryEngine engine) {
          this.cps2dep = cps2dep
          resource = cps2dep.deployment.eResource
          this.engine = engine
          prepare(engine)
          createTransformation
      }
  • Transformation will remain active until disposed is called (there is a generated dispose method in the class)

  • Create a rule to generate DeploymentHosts for each HostInstances

    • The BatchTransformationRuleFactory extension provides a builder API for rule definition

    • A VIATRA query is used as precondition to the rule, which means the rule will be activated each time the given pattern when changes allowing to update the output accordingly.

      val hostRule = createRule.precondition(HostInstanceMatcher.querySpecification).action[/*Action part*/].build
  • Specify which action to run when the rule fires. It will create the transformed DeploymentHost element in the output model as well as a trace element associating the source HostInstance and the target DeploymentHost:

    val hostRule = createRule.precondition(HostInstanceMatcher.querySpecification).action[
        val cpsHostInstance = it.hostInstance
        val nodeIp = cpsHostInstance.nodeIp
        println('''Mapping host with IP: «nodeIp»''')
    
        /** Create & initialize DeploymentHost in output model **/
        val depHost = cps2dep.deployment.createChild(deployment_Hosts, deploymentHost) => [
            set(deploymentHost_Ip, nodeIp)
        ]
    
        /** Create trace element in trace model **/
        cps2dep.createChild(CPSToDeployment_Traces, CPS2DeploymentTrace) => [
            addTo(CPS2DeploymentTrace_CpsElements, cpsHostInstance)
            addTo(CPS2DeploymentTrace_DeploymentElements, depHost)
        ]
    
        println('''Mapped with IP: «nodeIp»''')
    ].build
  • The rule which creates DeploymentApplication elements for ApplicationInstance objects, looks similar. It has to find the DeploymentHost created from the HostInstance to which the source ApplicationInstance is allocated, so it assumes the hostRule has already fired:

    val applicationRule = createRule.precondition(ApplicationInstanceMatcher.querySpecification).action[
        val cpsApplicationInstance = it.appInstance
        val appId = cpsApplicationInstance.identifier
        println('''Mapping application with ID: «appId»''')
    
        /* Find the DeploymentHost created from the HostInstance to which the source ApplicationInstance is allocated */
        val cpsHostInstance = cpsApplicationInstance.allocatedTo
        val depHost = engine.cps2depTrace.getAllValuesOfdepElement(null, null, cpsHostInstance).filter(DeploymentHost).head
        /* Create & initialize DeploymentApplication in this DeploymentHost */
        val deploymentApplication = depHost.createChild(deploymentHost_Applications, deploymentApplication) => [
            set(deploymentApplication_Id, appId)
        ]
    
        /* Create trace element in trace model */
        cps2dep.createChild(CPSToDeployment_Traces, CPS2DeploymentTrace) => [
            addTo(CPS2DeploymentTrace_CpsElements, cpsApplicationInstance)
            addTo(CPS2DeploymentTrace_DeploymentElements, deploymentApplication)
        ]
    
        println('''Mapped application with ID: «appId»''')
    ].build
  • Implement the method which performs the transformation using the rules defined above:

    • Since we are using the non-incremental (the whole model is always retransformed on model changes), the output and trace models are to be cleared before the any rule can fire

    • Pay attention to fire the rules in the proper order

      def execute() {
          println('''Executing transformation on: Cyber-physical system: «cps2dep.cps.identifier»''')
          /* Clear output & trace model for batch transformation**/
          cps2dep.deployment.hosts.clear
          cps2dep.traces.clear
          /* Fire transformation rules**/
          hostRule.fireAllCurrent
          applicationRule.fireAllCurrent
      }

4.1.2. Create a menu command to execute the transformation

  • Create a UI plugin with the following additional dependencies:

    org.eclipse.ui,
    com.incquerylabs.course.cps.viatra.batch;bundle-version="0.1.0",
    org.eclipse.viatra.examples.cps.traceability;bundle-version="0.1.0",
    org.eclipse.viatra.query.runtime;bundle-version="1.2.0"
  • Create handler implementation:

    TransformHandler.java
    public class TransformHandler extends AbstractHandler implements IHandler {
    
        ViatraQueryEngine engine;
        CPS2DeploymentTransformationViatra transformation;
    
        @Override
        public Object execute(ExecutionEvent event) throws ExecutionException {
            IStructuredSelection selection =
                (IStructuredSelection) HandlerUtil.getCurrentSelection(event);
    
            CPSToDeployment tracemodel =
                (CPSToDeployment) selection.getFirstElement();
    
            if (engine == null){
                try {
                    engine = ViatraQueryEngine.on(
                                new EMFScope(
                                    tracemodel.eResource().getResourceSet()));
                    transformation = new CPS2DeploymentTransformationViatra(tracemodel,
                                                                    engine);
                } catch (ViatraQueryException e) {
                    throw new ExecutionException(e.getMessage(), e);
                }
            }
            transformation.execute();
    
            return null;
        }
    
    }
  • Register handler in the context menu of CPSToDeployment elements in plugin.xml:

    <extension point="org.eclipse.ui.commands">
        <command
            defaultHandler="com.incquerylabs.course.cps.viatra.batch.ui.TransformHandler"
            id="com.incquerylabs.course.cps.viatra.batch.ui.command"
            name="Transform">
        </command>
    </extension>
    <extension point="org.eclipse.ui.menus">
        <menuContribution allPopups="false"
                locationURI="popup:org.eclipse.ui.popup.any?after=additions">
            <command commandId="com.incquerylabs.course.cps.viatra.batch.ui.command"
                    style="push">
                <visibleWhen checkEnabled="false">
                    <with variable="selection">
                        <count value="1">
                        </count>
                        <iterate>
                            <adapt type="org.eclipse.viatra.examples.cps.traceability.CPSToDeployment">
                            </adapt>
                        </iterate>
                    </with>
                </visibleWhen>
            </command>
        </menuContribution>
    </extension>

4.1.3. Execute the transformation

  • Launch Eclipse Application

  • Create a generic resource project

  • Copy a .cyberphysicalsystem resource in it if you already have one, or create a new CaberPhysicalSystem Model

    viatraIncr example1
    Figure 10. Project with a .cyberphysicalsystem resource
  • Create a Deployment model

    • Root element shall be Deployment

      viatraIncr example2
      Figure 11. New Deployment Model
  • Create a Traceability model

    • Root element shall be CPS To Deployment

      viatraIncr example3
      Figure 12. New Traceability Model
  • In the Traceability editor, load both CPS and Deployment models with Load Resources... in the context menu

    viatraIncr example4
    Figure 13. Load necessary resources into the Tracebility Model
  • Set CPS and Deployment references of traceability model in the properties view

    viatraIncr example5
    Figure 14. Set the references of the Traceability Model
  • Create a new HostType, HostInstance, ApplicationType and ApplicationInstance in the Deployment model

  • Execute transformation using the created command (on the context menu of the Traceability model root)

    viatrabatch
    Figure 15. Transformation command in the context menu

4.2. Event-driven Transformations

This exercise heps the audience to create a simple event-driven transformation using the VIATRA Transformation API. The transformation will create (and then incrementally update while active) a deployment model based on a CPS model. The exercise also covers registering a menu command which initializes the transformation.

Given the batch and event-driven transformations are really similar, this section focuses mainly on the differences; if required, consult the batch transformation tutorial.

4.2.1. Create transformation

Specific patterns have to be defined for event-driven rules; note that there are small differences to the batch definitions, e.g. there is an additional pattern called allocatedDeploymentApplication.

import "http://org.eclipse.viatra/model/cps"
import "http://org.eclipse.viatra/model/deployment"
import "http://org.eclipse.viatra/model/cps-traceability"

pattern hostInstance(hostInstance) {
    HostInstance(hostInstance);
}

pattern applicationInstance(appType, appInstance){
    HostInstance.applications(_, appInstance);
    ApplicationType.instances(appType, appInstance);
}

pattern allocatedDeploymentApplication(depHost, depApp) {
    DeploymentHost.applications(depHost, depApp);
}

pattern cps2depTrace(cps2dep, trace, cpsElement, depElement) {
    CPSToDeployment.traces(cps2dep, trace);
    CPS2DeploymentTrace.cpsElements(trace, cpsElement);
    CPS2DeploymentTrace.deploymentElements(trace, depElement);
}
  • Create transformation class (preferably Xtend)

    • Create new Model Transformation with the wizard

      transformation wizard
      Figure 16. Model Transformation Wizard - Create new transformation
      • Setup the name of the transformation and click Next

        eventdriven transformation wizard name
        Figure 17. Model Transformation Wizard - Name of the new batch transformation
      • Setup the type of the transformation to BatchTransformation and click Finish

        eventdriven transformation wizard type
        Figure 18. Model Transformation Wizard - Type of the new transformation
  • Register used, domain-specific APIs as extensions, common APIs are already generated

    /*
     * VIATRA Query group
     */
    val extension CpsXformM2M cpsXformM2M = CpsXformM2M.instance
    
    /*
     * EMF metamodels
     */
    val extension DeploymentPackage depPackage = DeploymentPackage::eINSTANCE
    val extension TraceabilityPackage trPackage = TraceabilityPackage::eINSTANCE
  • Constructor will also initialize transformation (replace the generated one)

    • It assumes that the output and trace models are already created

    • The IModelManipulations implementation is used to make model access replaceable, this way the same transformation may be used for cases where the resource set is transactional

      val CPSToDeployment cps2dep
      
      new(CPSToDeployment cps2dep, ViatraQueryEngine engine) {
          this.cps2dep = cps2dep
          this.resource = cps2dep.deployment.eResource
          this.engine = engine
          prepare(engine)
          createTransformation
      }
  • Transformation will remain active until dispose is called (a dispose method is already generated)

  • Create a rule to create DeploymentHosts for each HostInstances

    • The EventDrivenTransformationRuleFactory extension provides a builder API for rule definition

    • A VIATRA query pattern is used as precondition to the rule, which means the rule will be activated each time the given pattern changes allowing to update the output accordingly.

      val hostRule = createRule.precondition(HostInstanceMatcher.querySpecification)
  • Add action for each kind of changes in the pattern to update trace and output models:

    • upon creation of a HostInstance

      .action(CRUDActivationStateEnum.CREATED) [
          val hostinstance = hostInstance
          val nodeIp = hostInstance.nodeIp
          println('''Mapping host with IP: «nodeIp»''')
          /* Create new DeploymentHost element in output model */
          val host = cps2dep.deployment.createChild(deployment_Hosts, deploymentHost) => [
              set(deploymentHost_Ip, nodeIp)
          ]
          /* Create trace entry */
          cps2dep.createChild(CPSToDeployment_Traces, CPS2DeploymentTrace) => [
              addTo(CPS2DeploymentTrace_CpsElements, hostinstance)
              addTo(CPS2DeploymentTrace_DeploymentElements, host)
          ]
      ]
    • upon the change of a HostInstance

      .action(CRUDActivationStateEnum.UPDATED) [
          /* find associated DeploymentHost element */
          val depHost = engine.cps2depTrace
                              .getOneArbitraryMatch(cps2dep, null, hostInstance, null)
                              .depElement as DeploymentHost
          val hostIp = depHost.ip
          println('''Updating mapped host with IP: «hostIp»''')
          /* update IP attribute */
          val nodeIp = hostInstance.nodeIp
          depHost.set(deploymentHost_Ip, nodeIp)
          println('''Updated mapped host with IP: «nodeIp»''')
      ]
    • upon the removal of a HostInstance

      .action(CRUDActivationStateEnum.DELETED) [
          /* Find trace element */
          val traceMatch = engine.cps2depTrace
                              .getOneArbitraryMatch(cps2dep, null, hostInstance, null)
          val hostIp = hostInstance.nodeIp
          println('''Removing host with IP: «hostIp»''')
          /* Remove DeploymentHost element */
          cps2dep.deployment.remove(deployment_Hosts, traceMatch.depElement)
          /* Remove trace */
          cps2dep.remove(CPSToDeployment_Traces, traceMatch.trace)
          println('''Removed host with IP: «hostIp»''')
      ]
    • Add default activation lifecycle then build the rule:

      • The lifecycle defines the state machine used to determine the possible states on which transition actions can defined.

        .addLifeCycle(Lifecycles.getDefault(true, true)).build
  • The rule which create DeploymentApplication elements for ApplicationInstances, looks similar

    val applicationRule = createRule.precondition(ApplicationInstanceMatcher.querySpecification)
    .action(CRUDActivationStateEnum.CREATED) [
        /* Find associated DeploymentHost for the HostInstance this application is allocated to */
        val depHost = engine.cps2depTrace.getAllValuesOfdepElement(null, null, appInstance.allocatedTo).filter(
            DeploymentHost).head
        val appinstance = appInstance
        val appId = appInstance.identifier
        println('''Mapping application with ID: «appId»''')
        /* Create DeploymentApplication application in host */
        val app = depHost.createChild(deploymentHost_Applications, deploymentApplication) => [
            set(deploymentApplication_Id, appId)
        ]
        /* create trace entry */
        cps2dep.createChild(CPSToDeployment_Traces, CPS2DeploymentTrace) => [
            addTo(CPS2DeploymentTrace_CpsElements, appinstance)
            addTo(CPS2DeploymentTrace_DeploymentElements, app)
        ]
        println('''Mapped application with ID: «appId»''')
    ].action(CRUDActivationStateEnum.UPDATED) [
        /* find associated DeploymentApplication */
        val depApp = engine.cps2depTrace.getOneArbitraryMatch(cps2dep, null, appInstance, null).
            depElement as DeploymentApplication
        /* Update ID */
        if (depApp.id != appInstance.identifier)
            depApp.set(deploymentApplication_Id, appInstance.identifier)
    ].action(CRUDActivationStateEnum.DELETED) [
        /* find associated DeploymentApplication */
        val trace = engine.cps2depTrace.getAllValuesOftrace(null, appInstance, null).head as CPS2DeploymentTrace
        val depApp = trace.deploymentElements.head as DeploymentApplication
        /* Remove application from host */
        engine.allocatedDeploymentApplication.getAllValuesOfdepHost(depApp).head.remove(deploymentHost_Applications, depApp)
        /* Remove traces */
        cps2dep.remove(CPSToDeployment_Traces, trace)
    ].addLifeCycle(Lifecycles.getDefault(true, true)).build
  • Replace the generated createTransformation using the rules defined above

    • For cases when it is possible to have more than one rules activated (e.g. a new HostInstance is added to the model with already set allocated applications) a conflict resolver is used to provide a fixed ordering of rules to be executed.

    • We use a priority-based resolver (lower priority rules will be executed first), which considers priority of disappearing rules to be inverted (a disappearing application’s priority will be -2)

      private def createTransformation() {
          //Initialize model manipulation API
          this.manipulation = new SimpleModelManipulations(engine)
      
          //Initialize event-driven transformation
          val fixedPriorityResolver = new InvertedDisappearancePriorityConflictResolver
          fixedPriorityResolver.setPriority(hostRule.ruleSpecification, 1)
          fixedPriorityResolver.setPriority(applicationRule.ruleSpecification, 2)
      
          transformation = EventDrivenTransformation.forEngine(engine)
              .setConflictResolver(fixedPriorityResolver)
              .addRule(hostRule)
              .addRule(applicationRule)
              .build
      }

4.2.2. Creating a menu command to execute the transformation

  • Create UI plugin

  • Add dependencies:

    MANIFEST.MF
    org.eclipse.ui,
    com.incquerylabs.course.cps.viatra.incr;bundle-version="0.1.0",
    org.eclipse.viatra.examples.cps.traceability;bundle-version="0.1.0",
    org.eclipse.viatra.query.runtime;bundle-version="1.2.0"
  • Create handler implementations:

    ToggleTransformationHandler.java
    public class ToggleTransformationHandler extends AbstractHandler implements IHandler {
    
        ViatraQueryEngine engine;
        CPS2DeploymentTransformationViatra transformation;
    
    
        /* (non-Javadoc)
         * @see org.eclipse.core.commands.IHandler#execute(org.eclipse.core.commands.ExecutionEvent)
         */
        @Override
        public Object execute(ExecutionEvent event) throws ExecutionException {
            IStructuredSelection selection =
                (IStructuredSelection) HandlerUtil.getCurrentSelection(event);
    
            CPSToDeployment tracemodel =
                (CPSToDeployment) selection.getFirstElement();
    
            if(transformation == null) {
                if(engine == null) {
                    try {
                        engine = ViatraQueryEngine.on(
                                    new EMFScope(
                                        tracemodel.eResource()
                                                    .getResourceSet()));
                        transformation =
                            new CPS2DeploymentTransformationViatra(tracemodel,
                                                                    engine);
                    } catch (ViatraQueryException e) {
                        throw new ExecutionException(e.getMessage(), e);
                    }
                }
            } else {
                transformation.dispose();
            }
    
            return null;
        }
    
    }
  • Register handler in the context menu of "CPSToDeployment" elements

    plugin.xml
    <extension point="org.eclipse.ui.commands">
        <command defaultHandler="com.incquerylabs.course.cps.viatra.incr.ui.ToggleTransformationHandler"
                id="com.incquerylabs.course.cps.viatra.incr.ui.command"
                name="Toggle Transformation">
        </command>
    </extension>
    <extension point="org.eclipse.ui.menus">
        <menuContribution allPopups="false"
                locationURI="popup:org.eclipse.ui.popup.any?after=additions">
            <command commandId="com.incquerylabs.course.cps.viatra.incr.ui.command"
                    label="Toggle Incremental Transformation"
                    style="push">
                <visibleWhen checkEnabled="false">
                    <with variable="selection">
                        <count value="1">
                        </count>
                        <iterate>
                            <adapt type="org.eclipse.viatra.examples.cps.traceability.CPSToDeployment">
                            </adapt>
                        </iterate>
                    </with>
                </visibleWhen>
            </command>
        </menuContribution>
    </extension>

4.2.3. Executing the transformation

  • Launch runtime eclipse

  • Create a generic resource project

  • Copy a .cyberphysicalsystem resource in it

    viatraIncr example1
    Figure 19. Project with a .cyberphysicalsystem resource
  • Create an empty Deployment model

    • Root element shall be Deployment

      viatraIncr example2
      Figure 20. New Deployment Model
  • Create a Traceability model

    • Root element shall be "CPS To Deployment"

      viatraIncr example3
      Figure 21. New Traceability Model
  • In the Traceability editor, load both CPS and Deployment models with "Load Resources.." in the context menu

    viatraIncr example4
    Figure 22. Load necessary resources into the Traceability Model
  • Set CPS and Deployment references of traceability model in the properties view

    viatraIncr example5
    Figure 23. Set the references of the Traceability Model
  • Toggle transformation using the created command (on the context menu of the Traceability model root)

    viatraIncr example6
    Figure 24. Toggle transformation in the context menu
  • Initial activation done on first modification of the input model, e.g. create a new HostType

Back to the top