MWE2

The Modeling Workflow Engine 2 (MWE2) is a rewritten backwards compatible implementation of the Modeling Workflow Engine (MWE). It is a declarative, externally configurable generator engine. Users can describe arbitrary object compositions by means of a simple, concise syntax that allows to declare object instances, attribute values and references. One use case – that’s where the name had its origins – is the definition of workflows. Such a workflow consists usually of a number of components that interact with each other. There are components to read EMF resources, to perform operations (transformations) on them and to write them back or to generate any number of other artifacts out of the information. Workflows are typically executed in a single JVM. However there are no constraints the prevent implementors to provide components that spawn multiple threads or new processes.

Examples

Let’s start with a couple of examples to demonstrate some usage scenarios for MWE2. The first examples is a simple HelloWorld module that does nothing but print a message to standard out. The second module is assembled of three components that read an Ecore file, transform the contained classifier-names to upper-case and serialize the resource back to a new file. The last examples uses the life-cycle methods to print the execution time of the workflow.

The Simplest Workflow

The arguably shortest MWE2 module may look like the following snippet.

module HelloWorld 

SayHello {
  message = "Hello World!"
}

It configures a very simple workflow component with a message that should be printed to System.out when the workflow is executed. The module begins with a declaration of its name. It must fulfill the Java conventions for fully qualified class-names. That’s why the module HelloWorld has to be placed into the default package of a Java source folder. The second element in the module is the class-name SayHello which introduces the root element of the module. The interpreter will create an instance of the given type and configure it as declared between the curly braces. E.g. the assignment message = "Hello World!" in the module will be interpreted as an invocation of the setMessage(String) on the instantiated object. As one can easily imagine, the implementation of the class SayHello looks straight forward:

import org.eclipse.emf.mwe2.runtime.workflow.IWorkflowComponent;
import org.eclipse.emf.mwe2.runtime.workflow.IWorkflowContext;

public class SayHello implements IWorkflowComponent {

  private String message = "Hello World!";
  public void setMessage(String message) {
    this.message = message;
  }
  public String getMessage() {
    return message;
  }

  public void invoke(IWorkflowContext ctx) {
    System.out.println(getMessage());
  }

  public void postInvoke() {}
  public void preInvoke() {}
}

It looks like a simple POJO and that’s the philosophy behind MWE2. It is easily possible to assemble completely independent objects in a declarative manner. To make the workflow executable with the WorkflowRunner, the component SayHello must be nested in a root workflow:

module HelloWorld 

Workflow {
component = SayHello {
message = "Hello World!"
}
}

The class Workflow is actually org.eclipse.emf.mwe2.runtime.workflow.Workflow but its package is implicitly imported in MWE2 modules to make the the modules more concise. The execution result of this workflow will be revealed after a quick Run As .. -> MWE2 Workflow in the console as

Hello World!

A Simple Transformation

The following workflow solves the exemplary task to rename every EClassifier in an *.ecore file. It consists of three components that read, modify and write the model file:

module Renamer
Workflow {
  component = ResourceReader {
        uri = "model.ecore"
  }
  component = RenamingTransformer {}
  component = ResourceWriter {
        uri = "uppercaseModel.ecore"
  }
}

The implementation of these components is surprisingly simple. It is easily possible to create own components even for minor operations to automate a process.

The ResourceReader simply reads the file with the given URI and stores it in a so called slot of the workflow context. A slot can be understood as a dictionary or map-entry.

public class ResourceReader extends WorkflowComponentWithSlot {
  private String uri;
  public void invoke(IWorkflowContext ctx) {
    ResourceSet resourceSet = new ResourceSetImpl();
    URI fileURI = URI.createFileURI(uri); 
    Resource resource = resourceSet.getResource(fileURI, true);
    ctx.put(getSlot(), resource);
  }

  public void setUri(String uri) {
    this.uri = uri;
  }
  public String getUri() {
    return uri;
  }
}

The actual transformer takes the model from the slot and modifies it. It simply iterates the content of the resource, identifies each EClassifier and sets its name.

public class RenamingTransformer extends WorkflowComponentWithSlot {
  private boolean toLowerCase = false;
  public void invoke(IWorkflowContext ctx) {
    Resource resource = (Resource) ctx.get(getSlot());
    EcoreUtil.resolveAll(resource);
    Iterator<Object> contents = EcoreUtil.getAllContents(resource, true);
    Iterator<EClassifier> iter = 
        Iterators.filter(contents, EClassifier.class);
    while(iter.hasNext()) {
      EClassifier classifier = (EClassifier) iter.next();
      classifier.setName(isToLowerCase() 
          ? classifier.getName().toLowerCase()
          : classifier.getName().toUpperCase());
    }
  }

  public void setToLowerCase(boolean toLowerCase) {
    this.toLowerCase = toLowerCase;
  }
  public boolean isToLowerCase() {
    return toLowerCase;
  }
}

After the model has been modified it should be written to a new file. That’s what the ResourceWriter does. It actually takes the resource from the given slot and saves it with the configured URI:

public class ResourceWriter extends WorkflowComponentWithSlot {
  private String uri;
  public void invoke(IWorkflowContext ctx) {
    Resource resource = (Resource) ctx.get(getSlot());
    URI uri = URI.createFileURI(getUri());
    uri = resource.getResourceSet().getURIConverter().normalize(uri);
    resource.setURI(uri);
    try {
      resource.save(null);
    } catch (IOException e) {
      throw new WrappedException(e);
    }
  }

  public void setUri(String uri) {
    this.uri = uri;
  }
  public String getUri() {
    return uri;
  }
}

Last but not least, the common super-type for those components looks like this:

public abstract class WorkflowComponentWithSlot 
      implements IWorkflowComponent {
  private String slot = "model";
  public void setSlot(String slot) {
    this.slot = slot;
  }
  public String getSlot() {
    return slot;
  }

  public void postInvoke() {}
  public void preInvoke() {}
}

Each of the mentioned implementations is rather simple and can be done in a couple of minutes. This is true for many tedious tasks that developers face in their daily work. MWE2 can be used to automize these tasks with minimum effort.

A Stopwatch

The last example demonstrates how to combine the MWE2 concepts to create a simple stopwatch that allows to measure the execution time of a set of components. The idea is to add the very same stopwatch twice as a component to a workflow. It will measure the time from the first pre-invoke to the last post-invoke event and print the elapsed milliseconds to the console.

public class StopWatch implements IWorkflowComponent {
  private long start;
  private boolean shouldStop = false;
  public void invoke(IWorkflowContext ctx) {}

  public void postInvoke() {
    if (shouldStop) {
      long elapsed = System.currentTimeMillis() - start;
      System.out.println("Time elapsed: " + elapsed + " ms");
    }
    shouldStop = true;
  }

  public void preInvoke() {
    start = System.currentTimeMillis();
  }
}

Clients who want to leverage this kind of stopwatch may use the following pattern. The stopwatch-instance will be added as the first component and the last component to a workflow. Everything in between will be measured. In this case, it is another workflow that does not need know about this decoration. The idea is to use a local identifier for the instantiated StopWatch and reuse this one at the end to receive the post-invoke life-cycle event twice.

module MeasuredWorkflow

Workflow {
  component = StopWatch: stopWatch {}
  component = @OtherWorkflow {}
  component = stopWatch
}