Welcome to jMDA

To generate software automatically has been a strong ambition since the early days of software development.

jMDA is a new approach in this area. It streamlines proven, widely known and accepted open source technologies into a most comprehensible and easy to use set of Java libraries that are extremely powerful and flexible at the same time. The main purpose of jMDA is

  • to leverage a comprehensible and easy to use modelling environment,

  • to provide convenient and complete access to modelling information and

  • to make available easy to use software generator facilities.

The introduction will briefly explain the main drivers behind this project, the jMDA book provides more detailed information about the most important concepts and the open source software is available here.

NoDSL! – Domain Modelling with Java and Software Generation with jMDA

This post introduces many important concepts in jMDA without any ado. You will understand most parts of the example without explanation. The remaining parts will be briefly explained later in this post. The complete code and everything necessary to compile and run the example is available for download here.

The show case (generate Java Bean style classes from minimal Java types) might not be the most exciting one but it is fairly reasonable and – other MDD / MDSD approaches use similar introductory examples. This way you can easily compare jMDA with popular frameworks and tools like xText for example.

We start by introducing the domain. The key requirement is: For each Person in the domain there has to be a way to manage arbitrary textual Data that is organised in so called data items:

Picture 1: Domain Model for jMDA tutorial

So here is the complete model and the Java Bean generator in one place:

package de.jmda.sample.tutorial;

import static de.jmda.gen.LineIndenter.TAB_SINGLE;
import static de.jmda.gen.java.ModifiersUtil.VisibilityModifier.PRIVATE;
import static de.jmda.gen.java.ModifiersUtil.VisibilityModifier.PUBLIC;
import static de.jmda.gen.java.naming.ImportManagerProvider.getImportManager;
import static de.jmda.gen.java.naming.ImportManagerProvider.newImportManager;
import static de.jmda.gen.java.naming.ImportManagerProvider.restoreImportManager;
import static de.jmda.core.mproc.ProcessingUtilities.getFields;
import static de.jmda.core.mproc.ProcessingUtilities.getPackageQualifiedName;
import static de.jmda.core.mproc.ProcessingUtilities.getTypeElement;
import static de.jmda.core.util.CollectionsUtil.asSet;
import static de.jmda.core.util.StringUtil.firstLetterToUpperCase;

import de.jmda.gen.GeneratorException;
import de.jmda.gen.java.ClassGenerator;
import de.jmda.gen.java.CompilationUnitUtil;
import de.jmda.gen.java.FieldGenerator;
import de.jmda.gen.java.MethodGenerator;
import de.jmda.gen.java.impl.DefaultClassGenerator;
import de.jmda.gen.java.impl.DefaultInstanceFieldGenerator;
import de.jmda.gen.java.impl.DefaultInstanceMethodGenerator;
import de.jmda.gen.java.naming.ImportManager;
import de.jmda.core.mproc.task.AbstractTypeElementsTaskTypes;
import de.jmda.core.mproc.task.TaskException;
import de.jmda.core.mproc.task.TaskRunner;

// … further import statements omitted

/**
 * Demonstrates how to generate a Java Bean style class from a simple Java model.
 */
public class JUTBeanGenerator
{
  // domain model definition - start
  class Person
  {
    String firstname;
    String lastname;
    Date birthdate;
    List<Data> dataItems;
  }

  class Data
  {
    String dataItem;
  }
  // domain model definition - end

  // processor for domain model information
  private class Task extends AbstractTypeElementsTaskTypes
  {
    // constructor
    private Task(Set<? extends Class<?>> types) { super(types); }

    // process model domain information
    @Override public boolean execute() throws TaskException
    {
      Map<TypeElement, String> typeToTargetTypeName = new HashMap<>();

      String personTargetTypeName =
          Person.class.getPackage().getName() + "." + Person.class.getSimpleName();
      String dataTargetTypeName =
          Data.class.getPackage().getName() + "." + Data.class.getSimpleName();

      typeToTargetTypeName.put(getTypeElement(Person.class), personTargetTypeName);
      typeToTargetTypeName.put(getTypeElement(Data.class), dataTargetTypeName);

      // iterate types of domain model
      for (TypeElement type : getTypeElements())
      {
        // extract type's package and name
        String packagename = getPackageQualifiedName(type);
        String typename = type.getSimpleName().toString();

        // make backup of current import manager and create new import manager
        ImportManager backup =
            newImportManager(packagename, typename, typeToTargetTypeName);

        // create and configure a generator for the Java Bean to be generated
        ClassGenerator cg = new DefaultClassGenerator();

        cg.setPackageName(packagename);
        cg.setVisibility(PUBLIC);
        cg.setClassName(type.getSimpleName());

        // iterate the fields of the domain model type
        for (VariableElement field : getFields(type))
        {
          String fieldname = field.getSimpleName().toString();

          // create and configure a generator for the field declaration
          FieldGenerator fg = new DefaultInstanceFieldGenerator();

          fg.setVisibility(PRIVATE);
          fg.setTypeFrom(field);
          fg.setName(fieldname);
          fg.setLineIndenter(TAB_SINGLE);

          // add field generator to class generator
          cg.addFieldGenerator(fg);

          // create and configure a generator for the field getter method
          MethodGenerator mgg = new DefaultInstanceMethodGenerator();

          mgg.setVisibility(PUBLIC);
          mgg.setTypeFrom(field);
          mgg.setName("get" + firstLetterToUpperCase(fieldname));
          mgg.setMethodBody("return " + fieldname + ";");
          mgg.setLineIndenter(TAB_SINGLE);

          // add getter method generator to class generator
          cg.addMethodGenerator(mgg);

          // create and configure a generator for the field setter method
          MethodGenerator mgs = new DefaultInstanceMethodGenerator();

          mgs.setVisibility(PUBLIC);
          mgs.setName("set" + firstLetterToUpperCase(fieldname));
          mgs.addParameterFrom(field, field.getSimpleName());
          mgs.setMethodBody("this." + fieldname + " = " + fieldname + ";");
          mgs.setLineIndenter(TAB_SINGLE);

          // add setter method generator to class generator
          cg.addMethodGenerator(mgs);
        }

        // add generator for import statements to class generator
        cg.addImportStatementGenerators(
            getImportManager().getImportStatementGenerators());

        try
        {
          FileUtils.writeStringToFile(
              CompilationUnitUtil.buildFile("src/gen/java", packagename, typename),
              cg.generate().toString());
        }
        catch (GeneratorException | IOException e)
        {
          throw new TaskException("failure generating class for " + type, e);
        }
        finally
        {
          // restore formally import manager
          restoreImportManager(backup);
        }
      }

      return false;
    }
  }

  @Test
  public void test() throws IOException
  {
    Task task = new Task(asSet(Person.class, Data.class));
    TaskRunner.run(task);
  }
}
Example 1: NoDSL! - Domain Modelling with Java and Generating Software with jMDA

The complete example consists of a single Java class with three inner classes. It starts with import statements for types and methods of the jmda.core and jmda.gen libraries. Particularly the static imports allow for quite dense coding in the remaining part of the code.

The code continues with the definition of the domain model. The domain model is made up from two very simple Java type definitions, namely Person and Data.

Creating a domain model could hardly be easier. The model contains only a minimum amount of technical details. You can type the few lines that build up the model using any text editor. If you use a modern Java IDE you get the full power of Java source code editing including syntax highlighting, code assistance, refactoring support and many more for your modelling activities. With jMDA there is no need for the definition of a new proprietary language and the generation of a special tool set for that language!

Picture 1 shows a visual representation of the model. The UML class diagram was reverse engineered from the source code with the marvellous, free ObjectAid eclipse plugin.

Now back to the example source code. The definition of the domain model is directly followed by the inner class Task. In a sense tasks are the working horses for jMDA. To understand the example have a closer look at the execute method.

The execute method first iterates over the business objects of the domain model: Person and Data. Both model elements are represented by TypeElement instances. For each domain model element the execute method creates and configures a ClassGenerator object. It configures the package, the visibility and the class name for the element.

Then it examines the declared fields of the domain model element. For each field it performs three steps: instantiate and configure a generator for
  • a field declaration,
  • a getter method declaration and
  • a setter method declaration.
Each generator is instantiated with a default implementation of the respective interface. The default implementations provide a reasonable behaviour that can be configured to meet the particular requirements following the convention over configuration pattern.

Finally the declared generator objects are added to the class generator. The class generator takes care of things like the proper order of the added generators during the ClassGenerator.generate step.

There are only a few things that might need further explanation. The first thing is the import statements management. Simple Java source code generators always produce full qualified type names and don't care about reasonable import statement management. This way they repeatedly produce unnecessary qualified names that make the code very verbose and hard to read and understand.

jMDA provides an import management facility to overcome this problem. Usually you create an import manager for each Java type that is to be generated with newImportManager(...). Once an import manager has been created it transparently takes care about the types that are used in the generated code. Look at the generated import statements in the example.

In general the generation of Java types may be nested: You start with the generation of one Java type and before you finish that another nested Java type generation is started. To avoid side effects between multiple import manager instances it is strongly recommended to backup a possibly existing import manager before creating a new one and to restore the backed up manager when you are finished with the generation of a Java type (restoreImport­Manager(...)).

Finally CompilationUnitUtil helps with properly writing the generated Java types (that are StringBuffers until then) as .java files into the file system. If necessary it builds up the package folder hierarchy transparently.

That's it! Now let's have a look at the output of the class generators. First part is Person.java:

package de.jmda.sample.tutorial;

import java.util.Date;
import java.util.List;

public class Person
{
  private String firstname;
  private String lastname;
  private Date birthdate;
  private List<Data> dataItems;

  public String getFirstname()
  {
    return firstname;
  }

  public void setFirstname(String firstname)
  {
    this.firstname = firstname;
  }

  public String getLastname()
  {
    return lastname;
  }

  public void setLastname(String lastname)
  {
    this.lastname = lastname;
  }

  public Date getBirthdate()
  {
    return birthdate;
  }

  public void setBirthdate(Date birthdate)
  {
    this.birthdate = birthdate;
  }

  public List<Data> getDataItems()
  {
    return dataItems;
  }

  public void setDataItems(List<Data> dataItems)
  {
    this.dataItems = dataItems;
  }
}
Example 2: Generated Person Bean

And here comes Data.java:

package de.jmda.sample.tutorial;

public class Data
{
  private String dataItem;

  public String getDataItem()
  {
    return dataItem;
  }

  public void setDataItem(String dataItem)
  {
    this.dataItem = dataItem;
  }
}
Example 3: Generated Data Bean


No comments:

Post a Comment