Fractal | Fractal ADL | Cecilia Examples | Minus
 

Introduction

Purpose

This tutorial aims at introducing the task framework to the developers who intend to make extensions in the cecilia-adl toolchain. It should be considered as a complementary document to the task framework specification.

The subject of the tutorial is the implementation of an extremely simple code generator that generates a comment line for a given component name. The tutorial starts with a basic code generator class, and first illustrates how such a class can be transformed to a task. Then, the tutorial increments step by step the complexity of the application. Each step adds a new concept in the example. By the end, a composite task that provides a file containing the code generated for multiple components in a specified order is obtained.

We hope that, developers may enjoy each of these steps, and experiment them with their own use-cases.

Getting Started

  • First, download the source code of the tutorial from here task-component-2.1.0-tutorial.zip or here task-component-2.1.0-tutorial.tar.gz.
  • Extract the source code. A directory called task-component-2.1.0-tutorial will be created.
  • Command line with maven
    • Go into the task-component-2.1.0-tutorial folder.
    • Compile the source code with 'mvn compile'. Note that, each time you modify the source code, you need to recompile.
    • Execute one of the steps (say Step1) with 'mvn exec:java -Dexec.mainClass=org.objectweb.fractal.task.tutorial.steps.Step1'.
  • Eclipse (with maven plugin installed)
    • Import the project found in the task-component-2.1.0-tutorial into your Eclipse workspace.
    • The execution targets are already prepared for the project. To find them, open the dialog box 'Run->Open Run Dialog'. You should find the execution targets under the Java Application tree. Then, select one of them, and run it.

Step1: Creating a simple code generator

Before starting with the development of tasks, let's create a simple code generator class. Say this class generates a comment line for a given component name, and implements the CodeProvider interface, depicted below, to give access to the code that it generates.

public interface CodeProvider {
  String getCode();
}

The implementation of the SimpleCodeProvider class is depicted below. It gets the component name for which the comment should be generated as a parameter of its constructor, and returns the comment line that it generates each time its getCode() method is invoked.

public class SimpleCodeProvider implements CodeProvider {
  private final String componentName;

  public SimpleCodeProvider(final String componentName) {
    this.componentName = componentName;
  }

  public String getCode() {
    return "// Generated code for component '" + componentName + "'";
  }

}

To test the behavior of the SimpleCodeProvider say we create 10 instances of the latter class, each constructed with a different component name, and register them in a list. Once the list is filled, it is browsed and the registered interfaces are invoked. The result of those invocations are printed on the output console.

public class Step1 extends AbstractTutorialStep {
  public static void main(final String[] args) throws Exception {
    final List<CodeProvider> fileProviderList = new ArrayList<CodeProvider>();
    for (int i = 0; i < 10; i++) {
      final String component = "component-" + i;
      fileProviderList.add(new SimpleCodeProvider(component));
    }

    for (final CodeProvider fileProvider : fileProviderList) {
      System.out.println(fileProvider.getCode());
    }
  }
}

The output of the above test should be conform to the listing below.

// Generated code for component 'component-0'
// Generated code for component 'component-1'
// Generated code for component 'component-2'
// Generated code for component 'component-3'
// Generated code for component 'component-4'
// Generated code for component 'component-5'
// Generated code for component 'component-6'
// Generated code for component 'component-7'
// Generated code for component 'component-8'
// Generated code for component 'component-9'

One thing to recall from this step for the further steps is that created SimpleCodeProvider objects are registered in an ArrayList, and are invoked in the order that they have been registered. So that, the output is correctly ordered.

Step2: Transforming the SimpleCodeProvider into a task

The goal of this step is to transform the above SimpleCodeProvider into a task, so that it can be manipulated as an element of the task framework. According to the task framework, the tasks are Fractal components. That is, they interact with other tasks found in their environment through their server and client interfaces.

However, the tasks make use of an extended interface concept. In addition to their standard attributes such as name, signature, etc., they are attributed with a record. A record allows one to specify an identifier for an interface, that helps to distinguish the latter from the other interfaces found in its environment in an unambiguous way. A record is formed by a field/value pairs. It can contain as much of these pairs as needed. The values can be either static values known at compile time, or be parameters that are specified at runtime. A parameter is specified with a basic name. Each parameter that is used to assign a value to an interface record field, is considered as a parameter of the task that owns the interface. In other words, task are parameterized, and each record parameter should refer to a parameter of the task.

As explained in the task framework specification, primitive tasks are defined using Java Annotations. So far, we have a class implementing only one server interface. We choose to identify this interface with a record containing two fields:

  • role: the role of the interface. We will call this role as code-provider.
  • id : an identifier for this interface that will assign the interface to the component name for which it generates code. As this information is not known at compile-time, we will use a parameter called componentName for assigning a value to this parameter.

In summary, for transforming the previous SimpleCodeProvider class into a task, one need to add the annotations depicted below to the class definition.

@TaskParameters("componentName")
@ServerInterfaces(@ServerInterface(name = "code-provider", signature = CodeProvider.class, record = "role:code-provider, id:%", parameters = "componentName"))
public class SimpleCodeProviderTask implements CodeProvider {

Now, let's have a look on how these tasks can be instantiated and be executed. First, a TaskFactory component is needed to be available for instantiating tasks. The way this component is created or obtained is not subject of this document, and may depend on the toolchain architecture. However, curious developers may have a look in the AbstractTutorialStep class to have an idea of how it can be manually instantiated.

The below code piece illustrates the part differs from the Step1 for instantiating SimpleCodeProviderTask components. Indeed, an object of type SimpleCodeProviderTask needs to be created at first. The reference to this object is given to the task factory, as well as the list of values that are to be assigned to the task's parameters. The factory returns the Component interface of the created task component. This interface can be used for getting access to the server interfaces of the component.

final List<Component> codeProviderList = new ArrayList<Component>();
for (int i = 0; i < 10; i++) {
  final String component = "component-" + i;
  codeProviderList.add(taskFactory.newPrimitiveTask(
      new SimpleCodeProviderTask(component), component));
}

The below code piece illustrates for the execution of these tasks differ from the Step1. Indeed, one need to get access to the CodeProvider interface by introspecting the Component interface of the task. For that purpose, the name of the interface, as specified in the above annotation can be used. Once the CodeProvider interface is found using this reflection mechanism, it can be invoked as usual.

for (final Component component : codeProviderList) {
  final CodeProvider codeProviderItf = (CodeProvider) component
      .getFcInterface("code-provider");
  System.out.println(codeProviderItf.getCode());
}

Note that, as nothing has been modified in the behavior of the code provider task, the output of this step is strictly the same as the output if the Step1.

Step3: Transforming the SimpleCodeProviderTask to an executable task

The above SimpleCodeProviderTask regenerates its result each time its getMethod() method is invoked. This may raise some performance problem if this method is invoked many times during the execution of the toolchain, and the operation implemented by the task is more complex than returning a simple string object. To deal with this issue, the task framework provides the concept of executable tasks. That is, the task implements a specific interface, called Executable that is invoked the first time one of the server interfaces of the task is invoked. This way, the task can prepare its result the first time it is required, and return the same result each time its server interfaces are invoked.

The subject of this step is to transform the previous SimpleCodeProviderTask to such an Executable task. First of all, the class must implement the Executable interface. This requires the modification of the class specification line. Note that, there is no need to modify the class annotations (i.e. @ServerInterfaces annotation), sine the task framework's execution engine is able to discover whether a task is executable at runtime using reflection mechanisms.

public class ExecutableCodeProviderTask implements Executable, CodeProvider {

The below code piece illustrates the lines that differ in the implementation of the executable task. Indeed, the execute() method prepares the result, and the getCode() returns the result that is already prepared.

  private String result;

  public void execute() throws Exception {
    result = "// Generated code for component '" + componentName + "'";
  }

  public String getCode() {
    return result;
  }

Whether a task is executable or not is completely transparent from the user's point of view. Hence, the only line that differs in the implementation of the Step3 test class is the one that instantiates the task using an ExecutableCodeProviderTask object rather than a SimpleCodeProviderTask object.

codeProviderList.add(taskFactory.newPrimitiveTask(
    new ExecutableCodeProviderTask(component), component));

Naturally, no difference is observed on the output. Nevertheless, curious developers may use their debugger to check that the execute() method is automatically invoked the first time the getCode() method is invoked.

Step4: Encapsulating the set of code generator tasks within a composite task

This step is about the composite tasks. To keep it simple, we will create a composite task that encapsulates the above SimpleCodeProviderTask tasks, and export their interfaces.

As explained in the task framework specification, composite tasks are described using a specific language. Using this language, one can describe how the sub-tasks of the composite should be bound together, and which interfaces (either client or server) should be exported to the external environment.

The below listing illustrates the description of the composite task making subject of this step.

PlainExportComposition() {
  export server {role:"code-provider", id:component};
}

The above composition description contains only one rule. It says: export all the sub-component server interfaces which has a record corresponding to role of "code-provider" and that has a id field which is assigned to the component parameter. The architecture of an instance of composite component that will be created with this rule will be as depicted in the figure below. Note that, according to this rule, all the exported interfaces keep the same record.

The architecture of the composite that is obtained in Step4.

To test this step, let's create a composite encapsulating the previous ten code provider tasks. As depicted below, the first part of the test class that creates the primitive tasks is the as the previous steps. Once the list of primitive tasks (i.e. codeProviderList) is filled, a composite task is instantiated using the taskFactoryItf component. For that purpose, the list of sub-tasks as well as the name of the task composition descriptor is given to the task factory.

    final List<Component> subTaskList = new ArrayList<Component>();
    for (int i = 0; i < 10; i++) {
      final String component = "component-" + i;
      subTaskList.add(taskFactory.newPrimitiveTask(
          new SimpleCodeProviderTask(component), component));
    }

    final Component compositeTask = taskFactory.newCompositeTask(
        subTaskList, "tutorial.composites.PlainExportComposition",
        new HashMap<Object, Object>(), new Object[0]);

The newCompositeTask method returns the Component interface implemented by the instantiated composite component. As in the case of primitive tasks, this interface can be used to browse the functional interfaces of the component. As illustrated below, it is used for accessing the CodeProvider interfaces exported by the composite component. Needless to say, those interfaces can be invoked in the same way, as illustrated in the previous examples.

final Object[] interfaces = compositeTask.getFcInterfaces();
for (final Object itf : interfaces) {
  if (itf instanceof CodeProvider) {
    System.out.println(((CodeProvider) itf).getCode());
  }
}

When the test executable provided for this test is ran, one may notice that the order invocation of the CodeProvider interfaces are not the same as before. This is due to the fact that the list of interfaces of a composite task is not registered in an ordered list (i.e. typically a hash-list is used for better lookup performance). Reordering such interfaces makes subject of the Step8.

Finally, note that there exist predefined task ,so called ExportALL, that can be used for exporting all the client and server interfaces making part of composite task. This should have been used in replacement of the PlainExportComposition within the case of this example.

Step5: Modify records of interfaces that are exported by a composite

It is sometimes useful to export interfaces using another record than the original one in order to adapt the identifier of exported interfaces to the composition context of the external environment. For that purpose, the composition description language provides a way to modify the interface record when they are exported.

In this step, we simply propose to use the below composition description, called RenameExportComposition, instead of the one used for the previous step.

RenameExportComposition() {
  export server {role:"code-provider", id:component}
    as {role="renamed-code-provider", id=component};
}

As depicted in the below figure, the composite tasks that will be composed using the RenameExportComposition description will have their interface record roles renamed as renamed-code-provider, and their id will remain unchanged.

The architecture of the composite that is obtained in Step5.

Note that the record modification feature can be used as well for adding or removing fields of interface records.

Step6: Coupling code and file generator tasks

In this step, we propose to use all the features that are presented in the previous steps to set up something more useful: dumping the source code generated by code provider tasks into files. To deal will this issue, we will implement a new primitive task, that gets some code using a client interface, and that will provide two server interfaces, one FileProvider giving access to the generated file, and one CodeProvider to give access to a piece of code that provides a mean of using (i.e. including) the generated file.

The source code of the SimpleFileProvider task implementing the above behavior is given hereafter. Both FileProvider and SourceProvier server interfaces of this task are presented using the @ServerInterface annotations. In addition, a client interface is declared in order to get access to the code generated by another task. As the @ClientInterface is attached to the sourceCodeProviderItf field of type SourceCodeProvider, there is no need to duplicate the information about the interface signature within the annotation; it is detected automatically at runtime.

@TaskParameters("componentName")
@ServerInterfaces({
    @ServerInterface(name = "file-provider", signature = FileProvider.class, record = "role:file-provider, id:%", parameters = "componentName"),
    @ServerInterface(name = "include-provider", signature = CodeProvider.class, record = "role:include-provider, id:%", parameters = "componentName")})
public class SimpleFileProviderTask
    implements
      Executable,
      FileProvider,
      CodeProvider {

  @ClientInterface(name = "code-provider", record = "role:code-provider, id:%", parameters = "componentName")
  public CodeProvider  sourceCodeProviderItf;

  private final File   outDir;
  private final String fileName;

  public SimpleFileProviderTask(final String componentName, final File outDir) {
    this.outDir = outDir;
    this.fileName = componentName + ".adl.out";
  }

  private File   resultFile;
  private String resultCode;

  public void execute() throws Exception {
    resultFile = new File(outDir, fileName);
    try {
      final BufferedWriter out = new BufferedWriter(new FileWriter(resultFile));
      out.write(sourceCodeProviderItf.getCode());
      out.close();
    } catch (final IOException e) {
      e.printStackTrace();
    }
    resultCode = "#include \"" + fileName + "\"\n";
  }

  public File getFile() {
    return resultFile;
  }

  public String getCode() {
    return resultCode;
  }
}

Within the framework of this step, we will create as many SimpleFileProviderTask task as SimpleCodeProviderTask tasks, and bind them together to produce one file for each component. The following task composition description will be used for that purpose.

CodeFileComposition() {
  bind {role:"code-provider", id:component1}
    to {role:"code-provider", id:component2}
    where same(component1, component2);
    
  export server {role:"file-provider", id:component};
  
  export server {role:"include-provider", id:component}
    as {role="code-provider", id=component};
}

The first rule of this composition description specifies that all the client interfaces whose role is code-provider should be bound the server interface whose id is the same as the one of the client interface. This will create the one-to-one bindings between the ' SimpleCodeProvider and the SimpleFileProviderTask tasks as depicted in the below figure. The second rule says that all the server interfaces whose role is file-provider should be exported by the composite task. Finally, the last rule says that the include-provider interfaces (i.e. the CodeProvider interfaces that are implemented by the SimpleFileProviderTask task) should be exported as code-provider to the external environment. This way, this composite task can be transparently replaced by the composite task that is implemented in Step 4, from the point of view of the tasks which use the code-provider interfaces. Nevertheless, while the composite task created in Step4 provides directly the source code to its user, the composite task created in this step dumps the code into a file, and provides a piece of code that allows its user to access the code by including the generated file.

The architecture of the composite that is obtained in Step6.

The code for testing this step slightly differs from the previous one. That is, the SimpleFileProviderTask tasks should also be instantiated and registered in the list of sub-components as depicted below.

for (int i = 0; i < 10; i++) {
  final String component = "component-" + i;
  subTaskList.add(taskFactory.newPrimitiveTask(
      new SimpleCodeProviderTask(component), component));
  subTaskList.add(taskFactory.newPrimitiveTask(
      new SimpleFileProviderTask(component, outDir), component));
}

Finally, when the Step6 is executed, one can observe that a set of files are created in the folder called target/out/step6, and the code that allows including each of these files are printed on the console.

Step7: Aggregating the code generated by multiple tasks in one file

We propose in this step to modify the above scenario so that the code generated by the multiple SimpleCodeProviderTask tasks are put in one file. To implement this scenario, we will create only one instance of file provider task which has a collection client interface to access multiple code generators.

The below listing illustrates how the client interface for accessing the CodeProvider tasks can be declared as a collection interface. Indeed, it is sufficient to declare the interface field as a Map associating client interface names with CodeProvider interface references. Note that, the interface name which is denoted in this Map is the name of the client interface (which is created by the BindingController when a binding is requested), but not the name of the server interface. Therefore, this name cannot be used for selecting a given server interface among the others.

@ClientInterface(name = "code-provider", signature = CodeProvider.class, record = "role:code-provider")
public final Map<String, CodeProvider> sourceCodeProvidersItf = new HashMap<String, CodeProvider>();

The aggregation of the code generated by the set of CodeProvider tasks that are bound to this FileProvider task is done as follows.

for (final CodeProvider sourceCodeProviderItf : sourceCodeProvidersItf
    .values()) {
  out.append(sourceCodeProviderItf.getCode()).append('\n');
}

Another modification on the client interface is that, the id field in the record is no more needed, since this collection client interface is not specific to a specific code generator. Consequently, the composition description should also be slightly modified as below, so that all server code-provider server interfaces, whatever their id is, are bound to the client interface whose role is also code-provider.

bind {role:"code-provider"}
  to {role:"code-provider", id:component};

In order to test the behavior of this scenario, we will create 10 CodeProvider tasks, and only one FileProvider task (associated to a component called RootComponent, and apply the above composition rule, as depicted below:

    final List<Component> subTaskList = new ArrayList<Component>();
    for (int i = 0; i < 10; i++) {
      final String component = "component-" + i;
      subTaskList.add(taskFactory.newPrimitiveTask(new SimpleCodeProviderTask(
          component), component));
    }

    subTaskList.add(taskFactory.newPrimitiveTask(new AgregatedFileProviderTask(
        "RootComponent", outDir), "RootComponent"));
    final Component compositeTask = taskFactory.newCompositeTask(subTaskList,
        "tutorial.composites.AgregatedCodeFileComposition",
        new HashMap<Object, Object>(), new Object[0]);

The resulting composition scheme will be as depicted in the following figure.

The architecture of the composite that is obtained in Step7.

When the test is executed, one will observe that only one output file containing all the generated code is created in target/out/step7, and the include line for accessing this file will be printed in the console.

Step8: Aggregating the code generated by multiple tasks in one file in a given order

The previous step has illustrated the use of a collection client interface for aggregating the result of multiple CodeProvider tasks. However, as the collection interfaces doesn't allow distinguishing the different servers that are bound, the aggregator task cannot choose the order in which the code pieces are dumped into the file.

This step is about the @ClientForEach annotation, which allows creating a collection of client interfaces while keeping the ability of distinguishing them. Indeed, the @ClientForEach is shorthand for creating multiple singleton client interfaces which can be accessed using a Map. This way, one can iterate over these client interfaces as with the collection interfaces, and the key of the Map is a well defined iterator element that allows identifying the client interface to which it is associated.

The definition of such an interface is very similar to the creation of a collection client interface. The below listing illustrates the definition of the @ClientForEach interface in OrderedAgregatedFileProviderTask. The type of the interface field is a Map, whose key type is an Iterable type, and the value type is the type of the client interface (i.e. CodeProvider). An additional 'iterable' argument is specified as part of the @ClientForEach annotation. This is typically a parameter of the task on which the iteration over the client interfaces can be done.

@ClientInterfaceForEach(prefix = "code-provider", signature = CodeProvider.class, record = "role:code-provider, id:%", parameters = "componentList.element", iterable = "componentList")
public final Map<String, CodeProvider> sourceCodeProvidersItf = new HashMap<String, CodeProvider>();

The above iterable parameter can indeed be considered as a key set for iterating over the client interfaces. This key set should be given to the task by its creator. The way of giving this key set is typically using the constructor. The below listing illustrates the extension of the previous constructors with an additional parameter called subComponentList.

  private final File                     outDir;
  private final String                   fileName;
  private final List<String>             subComponentList;

  public OrderedAgregatedFileProviderTask(final String componentName,
      final File outDir, final List<String> subComponentList) {
    this.fileName = componentName + ".adl.out";
    this.outDir = outDir;
    this.subComponentList = subComponentList;
  }

The invocation of the client interfaces is depicted in the below listing. You can notice that, the iteration over the client interfaces is driven by the values found in the subComponentList that is used as the key set. This way, the client interfaces are accessed in the order that is provided by the subComponentList.

for (final String subComponentName : subComponentList) {
  out.append(sourceCodeProvidersItf.get(subComponentName).getCode())
      .append('\n');
}

Now, let's have a look on how the above key set is created. This is done in the test class for Step8. Indeed, an additional list, namely subComponentNameList, is filled during the creation of the CodeProvider tasks. This list is an ordered list containing the names of the components for which code will be generated. It is then given to the FileProvider task as parameter of its constructor. It is also given to the newPrimitiveTask since it makes part of the parameters of the task. Note that, there is no need to modify the composition rules, since the binding statement that is defined for the previous step is compatible as well with the records of the new client interface.

    final List<Component> subTaskList = new ArrayList<Component>();
    final List<String> subComponentNameList = new ArrayList<String>();
    for (int i = 0; i < 10; i++) {
      final String component = "component-" + i;
      subComponentNameList.add(component);
      subTaskList.add(taskFactory.newPrimitiveTask(new SimpleCodeProviderTask(
          component), component));
    }

    subTaskList.add(taskFactory.newPrimitiveTask(
        new OrderedAgregatedFileProviderTask("RootComponent", outDir,
            subComponentNameList), "RootComponent", subComponentNameList));

The below figure illustrates the architecture of the composite task that is created in this step. Note that the main difference compared to the previous step is that the client interface of the OrderedAgregatedFileProviderTask is considered as multiple singleton client interfaces, that can be distinguished by this task.

The architecture of the composite that is obtained in Step8.

Finally, when the test class Step8 is executed, one will observe that the console output is the same as the previous step, but the code printed in the file target/out/step7/RootComponent.adl.out is completely ordered.

 
2007-2009 © ObjectWeb Consortium  | Last Published: 2009-04-21 13:39  | Version: 2.1.0