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.

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

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

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 1. 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 2. Query Results View
Note
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.

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, behaving similarly to Java and accessing Java classes from the classpath of the project. In this case, the well-formedness of the address values is represented with a regular expression evaluation.

    • 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 {
          find ipFormatInvalid(host, ip);
      }
  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 : HostInstance, app : ApplicationInstance) {
          HostInstance.applications(host, app);
      }
      
      pattern countApplications(host : HostInstance, m : java Integer) {
          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);
          // Ensure the state is indeed included in the state machine; unnecessary in a well-formed model
          StateMachine.states(sm, state);
          // The + symbol after the pattern name represents transitive closure
          find connectedTo+(initial, state);
      }

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 : java String) {
    HostInstance.nodeIp(host1, commonIp);
    HostInstance.nodeIp(host2, commonIp);
    host1!=host2;
}

References