diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5e142013c2bdbc64d4fe3558748328a282bbfc7a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,183 @@
+#### joe made this: http://goel.io/joe
+
+#### scala ####
+*.class
+*.log
+
+
+#### sbt ####
+# Simple Build Tool
+# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
+
+dist/*
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+.history
+.cache
+.lib/
+
+
+#### jetbrains ####
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+
+#### java ####
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+
+#### eclipse ####
+
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+#### Custom (user defined) insertions ####
+.idea/
+.classpath
+.project
+output/
+doc/
+[lL]ocal[tT]est*
+
+# ANTLR
+
+gen/
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..b76c173210ab971ec9e4f94c18cdffca08361536
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,38 @@
+import com.simplytyped.Antlr4Plugin.autoImport.antlr4PackageName
+import sbt.Keys.{libraryDependencies, scalacOptions, version}
+
+val emfcommonVersion = "2.12.0"
+val emfecoreVersion = "2.12.0"
+val scoptVersion = "3.7.0"
+val liftVersion = "3.3.0"
+
+javacOptions ++= Seq("-encoding", "UTF-8")
+
+lazy val generator = (project in file("."))
+  .settings(
+    name := "CodeGenerator",
+    version := "0.1",
+    scalaVersion := "2.12.6",
+    libraryDependencies ++= Seq(
+      "org.scala-lang" % "scala-reflect" % scalaVersion.value,
+      "org.scala-lang" % "scala-compiler" % scalaVersion.value,
+      "org.eclipse.emf" % "org.eclipse.emf.common" % emfcommonVersion,
+      "org.eclipse.emf" % "org.eclipse.emf.ecore" % emfecoreVersion,
+      "com.github.scopt" %% "scopt" % scoptVersion,
+      "net.liftweb" %% "lift-json" % liftVersion,
+
+      "org.junit.platform" % "junit-platform-runner" % "1.0.0" % "test",
+      "org.junit.jupiter" % "junit-jupiter-engine" % "5.0.0" % "test",
+      "org.junit.vintage" % "junit-vintage-engine" % "4.12.0" % "test",
+      "org.assertj" % "assertj-core" % "3.12.2" % "test",
+
+      "net.aichler" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test
+    ),
+    scalacOptions ++= Seq(
+      "-language:implicitConversions"
+    ),
+  ).enablePlugins(Antlr4Plugin)
+
+antlr4PackageName in Antlr4 := Some("org.rosi_project.model_sync.model_join.representation.parser.antlr.generated")
+antlr4GenVisitor in Antlr4 := true
+antlr4GenListener in Antlr4 := false
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000000000000000000000000000000000000..cabf73b45107a5feb964e2c564c9687faafffdbe
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 1.2.7
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..4a65abadaf7e792155072db22cce49a4abbf4d54
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1,6 @@
+
+resolvers += Resolver.jcenterRepo
+
+addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.8.2")
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4")
+addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0")
diff --git a/project/sbt-antlr4.sbt b/project/sbt-antlr4.sbt
new file mode 100644
index 0000000000000000000000000000000000000000..7b26d1fc7b9ee12df151e57cb9e2528c1a4f8d9f
--- /dev/null
+++ b/project/sbt-antlr4.sbt
@@ -0,0 +1,2 @@
+
+addSbtPlugin("com.simplytyped" % "sbt-antlr4" % "0.8.2")
diff --git a/src/main/antlr4/ModelJoin.g4 b/src/main/antlr4/ModelJoin.g4
new file mode 100644
index 0000000000000000000000000000000000000000..70817b5b7a0eed652b51f26aaf916e280a5bbc90
--- /dev/null
+++ b/src/main/antlr4/ModelJoin.g4
@@ -0,0 +1,152 @@
+/*
+ * ModelJoin.g4
+ *
+ * Defines the structure of our ModelJoin grammar
+ *
+ * Author: Rico Bergmann
+ */
+grammar ModelJoin;
+
+/*
+ * Parser
+ */
+modeljoin : join+ EOF ;
+join : (naturaljoin | thetajoin | outerjoin) AS classres
+  ( OPENCURLYBRAKET
+    keepattributesexpr*
+    keepaggregatesexpr*
+    keepcalculatedexpr*
+    keepexpr*
+  CLOSEDCURLYBRAKET )? ;
+naturaljoin : NATURAL JOIN classres WITH classres ;
+thetajoin : THETA JOIN classres WITH classres WHERE oclcond;
+outerjoin : (leftouterjoin | rightouterjoin) OUTER JOIN classres WITH classres;
+leftouterjoin : LEFT ;
+rightouterjoin : RIGHT ;
+
+keepattributesexpr : KEEP ATTRIBUTES attrres (COMMA attrres)* ;
+keepaggregatesexpr : KEEP AGGREGATE aggrtype
+                      OPENBRAKET relattr CLOSEDBRAKET
+                      OVER classres AS attrres  ;
+keepcalculatedexpr  : KEEP CALCULATED ATTRIBUTE oclcond AS (typedattrres | attrres) ;
+keepexpr : (keeptypeexpr | keepoutgoingexpr | keepincomingexpr)
+  ( OPENCURLYBRAKET
+    keepattributesexpr*
+    keepaggregatesexpr*
+    keepcalculatedexpr*
+    keepexpr*
+  CLOSEDCURLYBRAKET)? ;
+keeptypeexpr : keepsupertypeexpr | keepsubtypeexpr ;
+keepsupertypeexpr : KEEP SUPERTYPE classres ( AS TYPE classres )? ;
+keepsubtypeexpr : KEEP SUBTYPE classres ( AS TYPE classres )? ;
+keepoutgoingexpr : KEEP OUTGOING attrres ( AS TYPE classres )? ;
+keepincomingexpr : KEEP INCOMING attrres (AS TYPE classres )? ;
+
+classres : WORD (DOT WORD)* ;
+attrres : WORD (DOT WORD)+ ;
+typedattrres  : attrres COLON WORD  ;
+relattr : WORD ;
+oclcond : (WORD | NUMBER | specialchar | anytoken | WHITESPACE | NEWLINE)+;
+aggrtype : SUM | AVG | MIN | MAX | SIZE ;
+
+specialchar : DOT
+              | OPENBRAKET
+              | CLOSEDBRAKET
+              | OPENCURLYBRAKET
+              | CLOSEDCURLYBRAKET
+              | COMMA
+              | UNDERSCORE
+              | SPECIALCHAR ;
+
+// anytoken matches all possible tokens in case their content may appear as part of some regular
+// text as well
+anytoken  : OPENBRAKET
+          | CLOSEDBRAKET
+          | OPENCURLYBRAKET
+          | CLOSEDCURLYBRAKET
+          | DOT
+          | COLON
+          | COMMA
+          | UNDERSCORE
+          | SPECIALCHAR
+
+          | JOIN
+          | NATURAL
+          | THETA
+          | WHERE
+          | OUTER
+          | RIGHT
+          | LEFT
+          | WITH
+          | AS
+
+          | KEEP
+          | ATTRIBUTES
+          | ATTRIBUTE
+          | AGGREGATE
+          | CALCULATED
+          | SUPERTYPE
+          | SUBTYPE
+          | OUTGOING
+          | INCOMING
+          | TYPE
+          | OVER
+
+          | SUM
+          | AVG
+          | MIN
+          | MAX
+          | SIZE ;
+
+/*
+ * Lexer
+ */
+
+fragment LOWERCASE  : [a-z]                 ;
+fragment UPPERCASE  : [A-Z]                 ;
+fragment ANYCASE    : LOWERCASE | UPPERCASE ;
+fragment DIGIT      : [0-9]                 ;
+
+OPENBRAKET        : '('         ;
+CLOSEDBRAKET      : ')'         ;
+OPENCURLYBRAKET   : '{'         ;
+CLOSEDCURLYBRAKET : '}'         ;
+DOT               : '.'         ;
+COLON             : ':'         ;
+COMMA             : ','         ;
+UNDERSCORE        : '_'         ;
+SPECIALCHAR       : [-><!="'|]  ;
+
+JOIN    : 'join'    ;
+NATURAL : 'natural' ;
+THETA   : 'theta'   ;
+WHERE   : 'where'   ;
+OUTER   : 'outer'   ;
+RIGHT   : 'right'   ;
+LEFT    : 'left'    ;
+WITH    : 'with'    ;
+AS      : 'as'      ;
+
+KEEP        : 'keep'        ;
+ATTRIBUTES  : 'attributes'  ;
+ATTRIBUTE   : 'attribute'   ;
+AGGREGATE   : 'aggregate'   ;
+CALCULATED  : 'calculated'  ;
+SUPERTYPE   : 'supertype'   ;
+SUBTYPE     : 'subtype'     ;
+OUTGOING    : 'outgoing'    ;
+INCOMING    : 'incoming'    ;
+TYPE        : 'type'        ;
+OVER        : 'over'        ;
+
+SUM   : 'sum'   ;
+AVG   : 'avg'   ;
+MIN   : 'min'   ;
+MAX   : 'max'   ;
+SIZE  : 'size'  ;
+
+WORD    : ANYCASE (ANYCASE | DIGIT | UNDERSCORE)* ;
+NUMBER  : [+-]? DIGIT+ DOT? DIGIT* ;
+
+WHITESPACE  : ' ' -> skip                   ;
+NEWLINE     : ('\r'? '\n' | '\r')+ -> skip  ;
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f28ca24b1a9e644ec9799541d88ef1d65c8e972
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/AttributePath.java
@@ -0,0 +1,147 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * An {@code AttributePath} represents a single attribute of some model class.
+ * <p>
+ * As <em>ModelJoin</em> does not care about types at a conceptual level, no type information is
+ * stored.
+ *
+ * @author Rico Bergmann
+ */
+public class AttributePath {
+
+  private static final String CLASS_ATTRIBUTE_DELIMITER = ".";
+
+  /**
+   * Generates a new {@code path} given the owning class as well as the attribute's name.
+   */
+  @Nonnull
+  public static AttributePath from(
+      @Nonnull ClassResource containingClass,
+      @Nonnull String attributeName) {
+    return new AttributePath(containingClass, attributeName);
+  }
+
+  /**
+   * Generates a new {@code path} given the owning class as well as the attribute.
+   */
+  @Nonnull
+  public static AttributePath from(
+      @Nonnull ClassResource containingClass,
+      @Nonnull RelativeAttribute attribute) {
+    return new AttributePath(containingClass, attribute);
+  }
+
+  public static AttributePath from(String containingClass, String attributeName) {
+    return new AttributePath(containingClass, attributeName);
+  }
+
+  /**
+   * Generates a new {@code AttributePath} by splitting up the {@code qualifiedPath} into the
+   * {@link ClassResource} and attribute portion. Therefore the {@code qualifiedPath} is expected
+   * to adhere to the following scheme: {@code [package].class.attribute}.
+   */
+  @Nonnull
+  public static AttributePath fromQualifiedPath(@Nonnull String qualifiedPath) {
+    final int splitPos = qualifiedPath.lastIndexOf(CLASS_ATTRIBUTE_DELIMITER);
+
+    if (splitPos < 0) {
+      throw new IllegalArgumentException("Missing class portion on '" + qualifiedPath + "'");
+    }
+
+    String classResourcePortion = qualifiedPath.substring(0, splitPos);
+    String attributePortion = qualifiedPath.substring(splitPos + 1);
+    return new AttributePath(ClassResource.fromQualifiedName(classResourcePortion), attributePortion);
+  }
+
+  @Nonnull
+  private final ClassResource containingClass;
+
+  @Nonnull
+  private final RelativeAttribute attribute;
+
+  public AttributePath(String containingClass, String attributeName) {
+    this.containingClass = ClassResource.fromQualifiedName(containingClass);
+    this.attribute = RelativeAttribute.of(attributeName);
+  }
+
+  /**
+   * Full constructor, parsing the attribute's name into a valid {@link RelativeAttribute}.
+   *
+   * @param containingClass the class which owns the attribute
+   * @param attributeName the attribute's name
+   */
+  public AttributePath(@Nonnull ClassResource containingClass, @Nonnull String attributeName) {
+    this.containingClass = containingClass;
+    this.attribute = RelativeAttribute.of(attributeName);
+  }
+
+  /**
+   * Full constructor.
+   *
+   * @param containingClass the class which owns the attribute
+   * @param attribute the attribute's name
+   */
+  public AttributePath(@Nonnull ClassResource containingClass, @Nonnull RelativeAttribute attribute) {
+    this.containingClass = containingClass;
+    this.attribute = attribute;
+  }
+
+  /**
+   * Creates a new {@code AttributePath} by copying another path.
+   */
+  protected AttributePath(@Nonnull AttributePath other) {
+    this.containingClass = other.getContainingClass();
+    this.attribute = other.getAttribute();
+  }
+
+  /**
+   * Provides the class which owns {@code this} attribute.
+   */
+  @Nonnull
+  public ClassResource getContainingClass() {
+    return containingClass;
+  }
+
+  /**
+   * Provides {@code this} attribute as a {@link RelativeAttribute}.
+   */
+  @Nonnull
+  public RelativeAttribute getAttribute() {
+    return attribute;
+  }
+
+  /**
+   * Provides the name of {@code this} attribute.
+   */
+  @Nonnull
+  public String getAttributeName() {
+    return attribute.getAttributeName();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof AttributePath)) {
+      return false;
+    }
+    AttributePath that = (AttributePath) o;
+    return containingClass.equals(that.containingClass) &&
+        attribute.equals(that.attribute);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(containingClass, attribute);
+  }
+
+  @Override
+  public String toString() {
+    return containingClass.toString() + "." + attribute;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..7143ad71f59935a361fbb255c576ad208ae7a1d3
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ClassResource.java
@@ -0,0 +1,86 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code ClassResource} represents a fully-qualified reference to some class to be used in a
+ * join. This class merely exist to emphasize that some resource actually represents a class and
+ * thus provides a nicer (i.e. better named) interface to query for the resource's fields.
+ * <p>
+ * In difference to a {@link ResourcePath}, instances of this class will drop the {@code .java}
+ * ending of the file name if referenced from the file system.
+ *
+ * @author Rico Bergmann
+ */
+public class ClassResource extends ResourcePath {
+
+  /**
+   * Generates a new {@code path} given the path to its containing <em>package</em> and the name of
+   * the class which should be referenced.
+   */
+  @Nonnull
+  public static ClassResource from(@Nonnull String packagePath, @Nonnull String className) {
+    return new ClassResource(packagePath, className);
+  }
+
+  /**
+   * @see ResourcePath#parse(String)
+   */
+  @Nonnull
+  public static ClassResource fromQualifiedName(@Nonnull String qualifiedClass) {
+    /*
+     * A class resource is just a better representation of a class (in difference to just _some_
+     * kind of resource) but works just the same - thus we will re-use all the parsing logic from
+     * the ResourcePath
+     */
+    ResourcePath parsedResource = ResourcePath.parse(qualifiedClass);
+    return new ClassResource(parsedResource.resourcePath, parsedResource.resourceName);
+  }
+
+  /**
+   * @see ResourcePath#parseAsFileSystemPath(String)
+   */
+  @Nonnull
+  public static ClassResource fromFileSystem(@Nonnull String fullPath) {
+    /*
+     * A class resource is just a better representation of a class (in difference to just _some_
+     * kind of resource) but works just the same - thus we will re-use all the parsing logic from
+     * the ResourcePath
+     */
+    ResourcePath parsedResource = ResourcePath.parseAsFileSystemPath(dropFileTypeEnding(fullPath));
+    return new ClassResource(parsedResource.resourcePath, parsedResource.resourceName);
+  }
+
+  /**
+   * Deletes the {@code .java} ending from a resource name.
+   */
+  private static String dropFileTypeEnding(String fullPath) {
+    return fullPath.replaceAll("\\.java$", "");
+  }
+
+  /**
+   * Full constructor.
+   *
+   * @param containingPackage the path to the package to which the class belongs
+   * @param className the name of the class
+   */
+  public ClassResource(@Nonnull String containingPackage, @Nonnull String className) {
+    super(containingPackage, className);
+  }
+
+  /**
+   * Provides the name of the class represented by {@code this} resource.
+   */
+  @Nonnull
+  public String getClassName() {
+    return resourceName;
+  }
+
+  /**
+   * Provides the package that contains the class represented by {@code this} resource.
+   */
+  @Nonnull
+  public String getPackage() {
+    return resourcePath;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f8afef2b4ba2623225ac2f1aed8ed72a6d838f5
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLConstraint.java
@@ -0,0 +1,32 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import javax.annotation.Nonnull;
+
+/**
+ * An {@code OCLConstraint} specifies some predicate on (possibly multiple) models that have to hold
+ * under certain conditions.
+ * <p>
+ * This class is a specialization of an arbitrary {@link OCLStatement} for statements, that
+ * evaluate to a boolean values.
+ *
+ * @author Rico Bergmann
+ */
+public class OCLConstraint extends OCLStatement {
+
+  /**
+   * Generates a new constraint with the given content.
+   */
+  @Nonnull
+  public static OCLConstraint of(@Nonnull String content) {
+    return new OCLConstraint(content);
+  }
+
+  /**
+   * Full constructor.
+   *
+   * @param content the actual constraint
+   */
+  public OCLConstraint(@Nonnull String content) {
+    super(content);
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java
new file mode 100644
index 0000000000000000000000000000000000000000..5acfc57b4c2c5ef660e5e9ee2ff6cc51a5d819dd
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/OCLStatement.java
@@ -0,0 +1,64 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * An {@code OCLStatement} is a simple expression written in the {@code Object Constraint Language}.
+ * <p>
+ * This class simply wraps an arbitrary OCL expression. No further checks or the like are performed.
+ *
+ * @author Rico Bergmann
+ * @see <a href="https://www.omg.org/spec/OCL/">OMG OCL specification</a>
+ */
+public class OCLStatement {
+
+  /**
+   * Creates a new statement with the given {@code content}.
+   */
+  @Nonnull
+  public static OCLStatement of(@Nonnull String content) {
+    return new OCLStatement(content);
+  }
+
+  @Nonnull
+  protected final String content;
+
+  /**
+   * Full constructor.
+   *
+   * @param content the actual expression
+   */
+  public OCLStatement(@Nonnull String content) {
+    this.content = content;
+  }
+
+  /**
+   * Provides the actual statement as a {@code String}.
+   */
+  public String get() {
+    return content;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    OCLStatement that = (OCLStatement) o;
+    return content.equals(that.content);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(content);
+  }
+
+  @Override
+  public String toString() {
+    return content;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3e002c63df945d8c681a987390ffa2b1b3150f3
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/RelativeAttribute.java
@@ -0,0 +1,78 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code RelativeAttribute} represents a member of some class.
+ * <p>
+ * In difference to the {@link AttributePath} this class does not retain any information about the
+ * class to which it belongs.
+ *
+ * @author Rico Bergmann
+ */
+public class RelativeAttribute {
+
+  /**
+   * Extracts the attribute from a qualified {@link AttributePath}.
+   */
+  @Nonnull
+  public static RelativeAttribute from(@Nonnull AttributePath attributePath) {
+    return new RelativeAttribute(attributePath.getAttributeName());
+  }
+
+  /**
+   * Generates a new {@code RelativeAttribute} with the given name.
+   */
+  @Nonnull
+  public static RelativeAttribute of(@Nonnull String attributeName) {
+    if (!attributeName.matches(ATTRIBUTE_NAME_REGEX)) {
+      throw new IllegalArgumentException("Not a valid attribute name: " + attributeName);
+    }
+    return new RelativeAttribute(attributeName);
+  }
+
+  private static final String ATTRIBUTE_NAME_REGEX = "^[a-zA-Z_]\\w*$";
+
+  @Nonnull
+  private final String attributeName;
+
+  /**
+   * Full constructor.
+   *
+   * @param attributeName the name of the attribute to generate
+   */
+  public RelativeAttribute(@Nonnull String attributeName) {
+    this.attributeName = attributeName;
+  }
+
+  /**
+   * Provides the name of the represented attribute.
+   */
+  @Nonnull
+  public String getAttributeName() {
+    return attributeName;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    RelativeAttribute that = (RelativeAttribute) o;
+    return attributeName.equals(that.attributeName);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(attributeName);
+  }
+
+  @Override
+  public String toString() {
+    return attributeName;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java
new file mode 100644
index 0000000000000000000000000000000000000000..d4f99857bc7c86cb284681e6853b86ae2f52a677
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/ResourcePath.java
@@ -0,0 +1,154 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import java.io.File;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code ResourcePath} represents a fully-qualified name of some model element to be used in a
+ * join.
+ *
+ * @author Rico Bergmann
+ */
+public class ResourcePath {
+
+  /**
+   * Generates a new {@code path} given the path to its containing <em>package</em> and the name of
+   * the resource which should be referenced.
+   */
+  @Nonnull
+  public static ResourcePath from(@Nonnull String packagePath, @Nonnull String resourceName) {
+    return new ResourcePath(packagePath, resourceName);
+  }
+
+  /**
+   * Generates a new {@code path} given its fully-qualified name.
+   *
+   * @param qualifiedResourcePath the path. It is expected to conform to the format {@code
+   *     pathToResource.resourceName}
+   */
+  @Nonnull
+  public static ResourcePath parse(@Nonnull String qualifiedResourcePath) {
+    if (qualifiedResourcePath.equals(".")) {
+      throw new IllegalArgumentException(
+          "Not a valid resource path: '" + qualifiedResourcePath + "'");
+    }
+
+    int splitPos = qualifiedResourcePath.lastIndexOf(".");
+
+    String packagePath;
+    if (splitPos == -1) {
+      packagePath = "";
+    } else {
+      packagePath = qualifiedResourcePath.substring(0, splitPos);
+    }
+
+    String resourceName = qualifiedResourcePath.substring(splitPos + 1);
+    return new ResourcePath(packagePath, resourceName);
+  }
+
+  /**
+   * Generates a new {@code path} given its fully-qualified name.
+   * <p>
+   * The name will be interpreted as some location on the file system (that is as {@code
+   * /path/to/resource}). However, both {@code \} as well as {@code /} will be accepted as path
+   * separators.
+   *
+   * @param qualifiedResourcePath the path. It is expected to conform to the format {@code
+   *     pathToResource/resourceName}
+   */
+  public static ResourcePath parseAsFileSystemPath(@Nonnull String qualifiedResourcePath) {
+    return parse(toPackageStylePath(qualifiedResourcePath));
+  }
+
+  /**
+   * Converts a path as used by the file system to the corresponding path as used in a Java package.
+   */
+  protected static String toPackageStylePath(@Nonnull String fileSystemResourcePath) {
+    return fileSystemResourcePath.replaceAll("[\\\\/]", ".");
+  }
+
+  @Nonnull
+  protected final String resourcePath;
+
+  @Nonnull
+  protected final String resourceName;
+
+  /**
+   * Full constructor.
+   *
+   * @param resourcePath the path to the directory/package in which the resource resides
+   * @param resourceName the name of the class (or other resource)
+   */
+  public ResourcePath(@Nonnull String resourcePath, @Nonnull String resourceName) {
+    if (resourceName.isEmpty()) {
+      throw new IllegalArgumentException("Resource name may not be empty");
+    }
+    this.resourcePath = resourcePath;
+    this.resourceName = resourceName;
+  }
+
+  /**
+   * Provides the path to the directory/package in which {@code this} resource resides. The path
+   * will be in "Java-package style", i.e. its components will be separated by {@code .}.
+   */
+  @Nonnull
+  public String getResourcePath() {
+    return resourcePath;
+  }
+
+  /**
+   * Provides the name of the resource which is represented by {@code this} path.
+   * <p>
+   * The name is relative to the containing directory/package.
+   */
+  @Nonnull
+  public String getResourceName() {
+    return resourceName;
+  }
+
+  /**
+   * Converts {@code this} path to {@code String}. It will match the format {@code package.name}.
+   */
+  @Nonnull
+  public String get() {
+    return this.toString();
+  }
+
+  /**
+   * Converts {@code this} path to a {@code String}, treating it as some path on the file system.
+   * <p>
+   * The components of the path will be separated by the platform-dependent separator for the
+   * current platform. That is, its result may vary depending on the OS on which it is run.
+   */
+  @Nonnull
+  public String getAsFileSystemPath() {
+    return this.toString().replaceAll("\\.", File.pathSeparator);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    ResourcePath that = (ResourcePath) o;
+    return this.resourcePath.equals(that.resourcePath) &&
+        this.resourceName.equals(that.resourceName);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(resourcePath, resourceName);
+  }
+
+  @Override
+  public String toString() {
+    if (resourcePath.isEmpty()) {
+      return resourceName;
+    }
+    return resourcePath + "." + resourceName;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java
new file mode 100644
index 0000000000000000000000000000000000000000..0384a2acf0bdb6fdacbbeb050a89c035d9a9fad1
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/core/TypedAttributePath.java
@@ -0,0 +1,95 @@
+package org.rosi_project.model_sync.model_join.representation.core;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code TypedAttributePath} extends a default {@link AttributePath} by adding type information,
+ * i.e. adding information about the type of the object the path references.
+ * <p>
+ * As we do not have any knowledge about the actual classes that are represented by paths we may
+ * not validate, whether the given type indeed matches the attribute's type.
+ *
+ * @author Rico Bergmann
+ */
+public class TypedAttributePath extends AttributePath {
+
+  private static final String PATH_TYPE_SEPARATOR = ":";
+
+  /**
+   * Creates a new {@code TypedAttributePath} by combining the provided path and type information.
+   */
+  @Nonnull
+  public static TypedAttributePath of(@Nonnull AttributePath path, @Nonnull ClassResource type) {
+    return new TypedAttributePath(path, type);
+  }
+
+  /**
+   * Generates a new {@code TypedAttributePath} by splitting up the given path into its path and
+   * type portion. The {@code qualifiedTypedPath} is expected to match the format {@code path:type},
+   * where {@code path} may be parsed using {@link AttributePath#fromQualifiedPath(String)} and type
+   * may be parsed using {@link ClassResource#fromQualifiedName(String)}.
+   */
+  @Nonnull
+  public static TypedAttributePath fromQualifiedTypedPath(@Nonnull String qualifiedTypedPath) {
+    final int pathTypeSplitPos = qualifiedTypedPath.lastIndexOf(PATH_TYPE_SEPARATOR);
+
+    if (pathTypeSplitPos < 0) {
+      throw new IllegalArgumentException("Missing type portion on '" + qualifiedTypedPath + "'");
+    }
+
+    final String qualifiedPath = qualifiedTypedPath.substring(0, pathTypeSplitPos);
+    final String type = qualifiedTypedPath.substring(pathTypeSplitPos + 1);
+    return new TypedAttributePath(AttributePath.fromQualifiedPath(qualifiedPath), ClassResource.fromQualifiedName(type));
+  }
+
+  @Nonnull
+  private final ClassResource type;
+
+  /**
+   * Full constructor.
+   *
+   * @param path the attribute
+   * @param type the attribute's type
+   */
+  public TypedAttributePath(
+      @Nonnull AttributePath path,
+      @Nonnull ClassResource type) {
+    super(path);
+    this.type = type;
+  }
+
+  /**
+   * Provides the type of {@code this} attribute.
+   */
+  @Nonnull
+  public ClassResource getType() {
+    return type;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TypedAttributePath)) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    TypedAttributePath that = (TypedAttributePath) o;
+    return type.equals(that.type);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), type);
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + PATH_TYPE_SEPARATOR + type.toString();
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e253dafd1feee21a329b2a2f65b4cb47488627f
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/CompoundKeepExpression.java
@@ -0,0 +1,20 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.List;
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code CompoundKeepExpression} is a special kind of {@code KeepExpression} which in turn may
+ * contain a number of other {@code KeepExpression}s.
+ *
+ * @author Rico Bergmann
+ */
+public abstract class CompoundKeepExpression extends KeepExpression {
+
+  /**
+   * Provides all the keep expressions that this expression is build of.
+   */
+  @Nonnull
+  public abstract List<KeepExpression> getKeeps();
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..ecdccf901a9aa31d861a66f11afdad9c78ba96e3
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/JoinExpression.java
@@ -0,0 +1,204 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+
+/**
+ * A {@code JoinExpression} combines two classes of the source models and
+ * generates a new view derived from them.
+ * <p>
+ * At a conceptual level, these classes are referred to as the {@code left} and
+ * {@code right} class.
+ * <p>
+ * Other than the classes to combine, each join also consists of a number of
+ * {@link KeepExpression}s which contain the fields which should form the
+ * resulting class as well as information on how to set their values.
+ * <p>
+ * As there are a number of different joins available, this class is left
+ * {@code abstract} with subclasses to represent the specific joins.
+ *
+ * @author Rico Bergmann
+ * @see NaturalJoinExpression
+ * @see OuterJoinExpression
+ * @see ThetaJoinExpression
+ */
+public abstract class JoinExpression implements Iterable<KeepExpression> {
+
+	/**
+	 * The {@code JoinType} defines which rules should be applied when
+	 * performing the join.
+	 */
+	public enum JoinType {
+
+		/**
+		 * The {@code natural join} combines two classes based on attributes
+		 * with equal name and type.
+		 *
+		 * @see NaturalJoinExpression
+		 */
+		NATURAL,
+
+		/**
+		 * The {@code outer join} works like the {@link #NATURAL} one, but
+		 * leaves instances from one class with no corresponding instance in the
+		 * other class according to the {@code outer join
+		 * type}. See the subclass for details.
+		 *
+		 * @see OuterJoinExpression
+		 */
+		OUTER,
+
+		/**
+		 * The {@code theta join} is more general than the {@link #NATURAL} and
+		 * {@link #OUTER} one as it enables an arbitrary criteria to define
+		 * whether two instances are "joinable" or not.
+		 *
+		 * @see ThetaJoinExpression
+		 */
+		THETA
+	}
+
+	@Nonnull
+	protected final ClassResource left;
+
+	@Nonnull
+	protected final ClassResource right;
+
+	@Nonnull
+	protected final ClassResource target;
+
+	@Nonnull
+	protected final List<KeepExpression> keeps;
+
+	/**
+	 * Full constructor.
+	 *
+	 * @param left
+	 *            the left class to use in the join
+	 * @param right
+	 *            the right class to use in the join
+	 * @param target
+	 *            the name of the resulting view
+	 * @param keeps
+	 *            the keep statements in the join
+	 */
+	protected JoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right, @Nonnull ClassResource target,
+			@Nonnull List<KeepExpression> keeps) {
+
+		this.left = left;
+		this.right = right;
+		this.target = target;
+		this.keeps = keeps;
+	}
+
+	/**
+	 * Proof if the right and the left class path are equals.
+	 * @return result value
+	 */
+	public boolean isSameElement() {
+		if (left.getResourceName().equals(right.getResourceName())
+				&& left.getResourcePath().equals(right.getResourcePath())) {
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Provides the based class used in {@code this} join. Only important for
+	 * outer join in other cases always Left.
+	 */
+	@Nonnull
+	public ClassResource getBaseModel() {
+		return this.getLeft();
+	}
+
+	/**
+	 * Provides the other class used in {@code this} join. Only important for
+	 * outer join in other cases always Right.
+	 */
+	@Nonnull
+	public ClassResource getOtherModel() {
+		return this.getRight();
+	}
+
+	/**
+	 * Provides the left class used in {@code this} join.
+	 */
+	@Nonnull
+	public ClassResource getLeft() {
+		return left;
+	}
+
+	/**
+	 * Provides the right class used in {@code this} join.
+	 */
+	@Nonnull
+	public ClassResource getRight() {
+		return right;
+	}
+
+	/**
+	 * Provides the name of the class resulting from {@code this} join.
+	 */
+	@Nonnull
+	public ClassResource getTarget() {
+		return target;
+	}
+
+	/**
+	 * Provides the {@code keep} statements that should be used to build the
+	 * resulting class.
+	 */
+	@Nonnull
+	public Iterable<KeepExpression> getKeeps() {
+		return keeps;
+	}
+
+	public List<KeepExpression> getKeepsList() {
+		return Collections.unmodifiableList(keeps);
+	}
+
+	/**
+	 * Provides the type of {@code this} join.
+	 */
+	@Nonnull
+	public abstract JoinType getType();
+
+	/**
+	 * Provides the type of {@code this} join as a non-technical {@code String}.
+	 */
+	@Nonnull
+	abstract String getJoinTypeAsString();
+
+	@Nonnull
+	@Override
+	public Iterator<KeepExpression> iterator() {
+		return keeps.iterator();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (this == o) {
+			return true;
+		}
+		if (o == null || getClass() != o.getClass()) {
+			return false;
+		}
+		JoinExpression that = (JoinExpression) o;
+		return this.target.equals(that.target);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(target);
+	}
+
+	@Override
+	public String toString() {
+		return getJoinTypeAsString() + " " + left.toString() + " with " + right.toString() + " as " + target.toString();
+	}
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0da5aa0b03a4d87b7cacf86d1d7c8442e577eb1
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAggregateExpression.java
@@ -0,0 +1,261 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.RelativeAttribute;
+
+/**
+ * A {@code KeepAggregateExpression} adds an attribute to some join by calculating an aggregation on
+ * the referenced collection of numerical values. The possible aggregation operations are equal to
+ * those provided by the SQL programming language.
+ * <p>
+ * The aggregate's result will in turn be some numerical value.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepAggregateExpression extends KeepExpression {
+
+  /**
+   * The {@code AggregationType} defines what aggregation should be performed.
+   */
+  public enum AggregationType {
+
+    /**
+     * The attribute's value should be the sum of all the referenced values.
+     */
+    SUM,
+
+    /**
+     * The attribute's value should be the average value of all the referenced values.
+     */
+    AVG,
+
+    /**
+     * The attribute's value should be the minimum value of all the referenced values.
+     */
+    MIN,
+
+    /**
+     * The attribute's value should be the maximum value of all the referenced values.
+     */
+    MAX,
+
+    /**
+     * The attribute's value should be the number of referenced elements.
+     */
+    SIZE
+  }
+
+  /**
+   * The {@code AggregateBuilder} enables the construction of new {@link KeepAggregateExpression}s
+   * through a nice and fluent interface.
+   *
+   * @author Rico Bergmann
+   */
+  public static class AggregateBuilder {
+
+    @Nonnull
+    private final AggregationType aggregationType;
+
+    @Nonnull
+    private final RelativeAttribute aggregatedAttribute;
+
+    @Nonnull
+    private AttributePath source;
+
+    /**
+     * Full constructor.
+     *
+     * @param aggregationType the aggregation operation that should be performed
+     * @param aggregatedAttribute the attribute on which the aggregation should be performed
+     */
+    AggregateBuilder(@Nonnull AggregationType aggregationType,
+        @Nonnull RelativeAttribute aggregatedAttribute) {
+      this.aggregationType = aggregationType;
+      this.aggregatedAttribute = aggregatedAttribute;
+    }
+
+    /**
+     * Specifies the reference destination that provides the attribute to join.
+     * <p>
+     * Suppose the following example:
+     * <pre>
+     * {@code
+     *  class City {
+     *    List<Street> streets;
+     *  }
+     *
+     *  class Street {
+     *    int length;
+     *  }
+     * }
+     * </pre>
+     * If one was to calculate the average length of all streets per city, the following aggregation
+     * could be used: {@code keep aggregate avg(length) over City.streets as avgStreetLength}. In
+     * this case, {@code City.streets} is the wanted reference destination as it provides the {@code
+     * length} attribute which should be used for the aggregation.
+     */
+    @Nonnull
+    public AggregateBuilder over(@Nonnull AttributePath source) {
+      this.source = source;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the aggregation attribute in the target join.
+     */
+    @Nonnull
+    public KeepAggregateExpression as(@Nonnull AttributePath target) {
+      return new KeepAggregateExpression(aggregationType, aggregatedAttribute, source, target);
+    }
+  }
+
+  /**
+   * Generates a new {@code keep aggregate} expression that sums up the referenced values.
+   */
+  @Nonnull
+  public static AggregateBuilder sum(@Nonnull RelativeAttribute attribute) {
+    return new AggregateBuilder(AggregationType.SUM, attribute);
+  }
+
+  /**
+   * Generates a new {@code keep aggregate} expression that calculates the average value of the
+   * referenced values.
+   */
+  @Nonnull
+  public static AggregateBuilder avg(@Nonnull RelativeAttribute attribute) {
+    return new AggregateBuilder(AggregationType.AVG, attribute);
+  }
+
+  /**
+   * Generates a new {@code keep aggregate} expression that provides the minimum value of the
+   * referenced values.
+   */
+  @Nonnull
+  public static AggregateBuilder min(@Nonnull RelativeAttribute attribute) {
+    return new AggregateBuilder(AggregationType.MIN, attribute);
+  }
+
+  /**
+   * Generates a new {@code keep aggregate} expression that provides the maximum value of the
+   * referenced values.
+   */
+  @Nonnull
+  public static AggregateBuilder max(@Nonnull RelativeAttribute attribute) {
+    return new AggregateBuilder(AggregationType.MAX, attribute);
+  }
+
+  /**
+   * Generates a new {@code keep aggregate} expression that counts the number of values that are
+   * referenced.
+   */
+  @Nonnull
+  public static AggregateBuilder size(@Nonnull RelativeAttribute attribute) {
+    return new AggregateBuilder(AggregationType.SIZE, attribute);
+  }
+
+  @Nonnull
+  private final AggregationType aggregation;
+
+  @Nonnull
+  private final RelativeAttribute aggregatedAttribute;
+
+  @Nonnull
+  private final AttributePath source;
+
+  @Nonnull
+  private final AttributePath target;
+
+  /**
+   * Full constructor.
+   *
+   * @param aggregation the aggregation operation that should be performed
+   * @param aggregatedAttribute the attribute on which the aggregation should be performed
+   * @param source the field which provides the {@code aggregatedAttribute}. Therefore this
+   *     attribute has to be multi-valued.
+   * @param target the name of the attribute under which the aggregation result should be
+   *     available in the join
+   */
+  public KeepAggregateExpression(
+      @Nonnull AggregationType aggregation,
+      @Nonnull RelativeAttribute aggregatedAttribute,
+      @Nonnull AttributePath source,
+      @Nonnull AttributePath target) {
+    this.aggregation = aggregation;
+    this.aggregatedAttribute = aggregatedAttribute;
+    this.source = source;
+    this.target = target;
+  }
+
+  /**
+   * Provides the aggregation operation {@code this} expression will perform.
+   */
+  @Nonnull
+  public AggregationType getAggregation() {
+    return aggregation;
+  }
+
+  /**
+   * Provides the attribute that will be aggregated. As all {@link AggregationType aggregation
+   * operations} are defined on numerical values, this attribute in turn has to be of numerical
+   * type.
+   */
+  @Nonnull
+  public RelativeAttribute getAggregatedAttribute() {
+    return aggregatedAttribute;
+  }
+
+  /**
+   * Provides the field which contains the {@link #getAggregatedAttribute() aggregated attribute}.
+   * As an aggregation will - by definition - be performed upon an arbitrary number of values, this
+   * attribute has to be some kind of collection.
+   */
+  @Nonnull
+  public AttributePath getSource() {
+    return source;
+  }
+
+  /**
+   * Provides the attribute under which the result of {@code this} aggregation should be made
+   * available.
+   */
+  @Nonnull
+  public AttributePath getTarget() {
+    return target;
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepAggregateExpression that = (KeepAggregateExpression) o;
+    return aggregation == that.aggregation &&
+        aggregatedAttribute.equals(that.aggregatedAttribute) &&
+        source.equals(that.source) &&
+        target.equals(that.target);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(aggregation, aggregatedAttribute, source, target);
+  }
+
+  @Override
+  public String toString() {
+    String aggregationOperation = aggregation.toString().toLowerCase();
+    return "keep aggregate " //
+        + aggregationOperation + "(" + aggregatedAttribute + ") " //
+        + "over" + source //
+        + " as " + target;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..99cdb8109e16e584442425b0eac0aceefbe49337
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepAttributesExpression.java
@@ -0,0 +1,82 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+
+/**
+ * A {@code KeepAttributesExpression} simply retains a number of attributes from arbitrary source
+ * classes and includes them in the target join. The attributes' name and type will be left
+ * unchanged.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepAttributesExpression extends KeepExpression {
+
+  /**
+   * Generates a new keep expression for a number of attributes.
+   */
+  @Nonnull
+  public static KeepAttributesExpression keepAttributes(@Nonnull AttributePath firstAttribute,
+      @Nonnull AttributePath... moreAttributes) {
+    List<AttributePath> attributes = Lists.newArrayList(moreAttributes);
+    attributes.add(0, firstAttribute);
+    return new KeepAttributesExpression(attributes);
+  }
+
+  @Nonnull
+  private final List<AttributePath> attributes;
+
+  /**
+   * Full constructor.
+   *
+   * @param attributes the attributes for which the expression should be created
+   */
+  public KeepAttributesExpression(@Nonnull List<AttributePath> attributes) {
+    this.attributes = attributes;
+  }
+
+  /**
+   * Provides the attributes that should be retained in the join.
+   * <p>
+   * The returned {@code List} is a shallow copy of the attributes in {@code this} expression.
+   */
+  @Nonnull
+  public List<AttributePath> getAttributes() {
+    return new ArrayList<>(attributes);
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepAttributesExpression that = (KeepAttributesExpression) o;
+    return this.attributes.equals(that.attributes);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(attributes);
+  }
+
+  @Override
+  public String toString() {
+    return "keep attributes " + attributes.stream() //
+        .map(AttributePath::toString) //
+        .reduce((firstAttribute, secondAttribute) -> firstAttribute + ", " + secondAttribute) //
+        .orElseThrow(() -> new IllegalStateException(
+            "A keep attribute expression has to keep at least one attribute"));
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..19a1a22247fbbb23dcbcb582746b528897fc5423
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepCalculatedExpression.java
@@ -0,0 +1,125 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.OCLStatement;
+import org.rosi_project.model_sync.model_join.representation.core.TypedAttributePath;
+
+/**
+ * A {@code KeepCalculatedExpression} adds an attribute to some join by evaluating an arbitrary
+ * {@link OCLStatement OCL statement}.
+ * <p>
+ * This is the most general kind of {@link KeepExpression} as all other expressions may be expressed
+ * in OCL as well.
+ *
+ * @author Rico Bergmann
+ * @see OCLStatement
+ */
+public class KeepCalculatedExpression extends KeepExpression {
+
+  /**
+   * The {@code KeepCalculatedBuilder} enables the construction of new {@link
+   * KeepCalculatedExpression}s through a nice and fluent interface.
+   *
+   * @author Rico Bergmann
+   */
+  public static class KeepCalculatedBuilder {
+
+    @Nonnull
+    private final OCLStatement calculationRule;
+
+    /**
+     * Full constructor.
+     *
+     * @param calculationRule the OCL statement that should be used to calculate the attribute's
+     *     value.
+     */
+    KeepCalculatedBuilder(@Nonnull OCLStatement calculationRule) {
+      this.calculationRule = calculationRule;
+    }
+
+    /**
+     * Specifies the name of the attribute in the target join.
+     */
+    @Nonnull
+    public KeepCalculatedExpression as(@Nonnull TypedAttributePath target) {
+      return new KeepCalculatedExpression(calculationRule, target);
+    }
+  }
+
+  /**
+   * Generates a new {@code keep calculated} expression.
+   */
+  @Nonnull
+  public static KeepCalculatedBuilder keepCalculatedAttribute(
+      @Nonnull OCLStatement calculationRule) {
+    return new KeepCalculatedBuilder(calculationRule);
+  }
+
+  @Nonnull
+  private final OCLStatement calculationRule;
+
+  @Nonnull
+  private final TypedAttributePath target;
+
+  /**
+   * Full constructor.
+   *
+   * @param calculationRule the OCL statement that should be used to calculate the attribute's
+   *     value
+   * @param target the name of the attribute under which the aggregation result should be
+   *     available in the join
+   */
+  public KeepCalculatedExpression(
+      @Nonnull OCLStatement calculationRule,
+      @Nonnull TypedAttributePath target) {
+    this.calculationRule = calculationRule;
+    this.target = target;
+  }
+
+  /**
+   * Provides the OCL expression that is used to determine the value of {@code this} attribute.
+   */
+  @Nonnull
+  public OCLStatement getCalculationRule() {
+    return calculationRule;
+  }
+
+  /**
+   * Provides the attribute under which the result of {@code this} calculation should be made
+   * available.
+   */
+  @Nonnull
+  public TypedAttributePath getTarget() {
+    return target;
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepCalculatedExpression that = (KeepCalculatedExpression) o;
+    return calculationRule.equals(that.calculationRule) &&
+        target.equals(that.target);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(calculationRule, target);
+  }
+
+  @Override
+  public String toString() {
+    return "keep calculated attribute " + calculationRule + " as " + target;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd879001f642b3dc1a5beef04e196f86534e97ab
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepExpression.java
@@ -0,0 +1,60 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A {@code KeepExpression} configures the resulting model of a join.
+ * <p>
+ * This mostly means retaining some attributes or calculating their value but may also include
+ * maintaining references to other classes.
+ *
+ * @author Rico Bergmann
+ */
+public abstract class KeepExpression {
+
+  /**
+   * The {@code visitor} enables the execution of arbitrary algorithms on {@code KeepExpression}s
+   * without taking care of the actual traversal of the expression tree.
+   */
+  public interface KeepExpressionVisitor {
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepAggregateExpression}s.
+     */
+    void visit(@Nonnull KeepAggregateExpression expression);
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepAttributesExpression}s.
+     */
+    void visit(@Nonnull KeepAttributesExpression expression);
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepCalculatedExpression}s.
+     */
+    void visit(@Nonnull KeepCalculatedExpression expression);
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepReferenceExpression}s.
+     */
+    void visit(@Nonnull KeepReferenceExpression expression);
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepSubTypeExpression}s.
+     */
+    void visit(@Nonnull KeepSubTypeExpression expression);
+
+    /**
+     * Runs the algorithm as appropriate for {@link KeepSuperTypeExpression}s.
+     */
+    void visit(@Nonnull KeepSuperTypeExpression expression);
+
+  }
+
+  // TODO `as` statements may be omitted !?!? ... (╯°□°)╯︵ ┻━┻
+
+  /**
+   * Applies a {@code visitor} to {@code this} expression structure.
+   */
+  public abstract void accept(@Nonnull KeepExpressionVisitor visitor);
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d6f0f32924f2f495b21b9ab3107dcbf15c044b2
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepReferenceExpression.java
@@ -0,0 +1,223 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.util.Assert;
+
+// TODO the Xtext grammar also specifies an optional `as reference` part - what does that part do?
+// most likely `as reference` will refer to another type that was already defined in its dedicated
+// join statement.
+
+/**
+ * A {@code KeepReferenceExpression} retains links to other instances in the original models. All
+ * objects that are referenced in the base models will also be available in the resulting ModelJoin
+ * views.
+ * <p>
+ * As references are directional, each expression also distinguishes between {@code outgoing} and
+ * {@code incoming} references - depending on whether the model class of {@code this} expression
+ * owns the reference ({@code outgoing}) or instances of this model class are being referenced
+ * ({@code incoming}).
+ * <p>
+ * The referenced instances will be made available according to a number of keep statements which
+ * specify the attributes to include. This boils down to the creation of a nested ModelJoin view for
+ * these instances.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepReferenceExpression extends CompoundKeepExpression {
+
+  /**
+   * The {@code ReferenceDirection} defines whether a reference is owned by the containing model
+   * class or whether it is the class being referenced.
+   */
+  public enum ReferenceDirection {
+
+    /**
+     * Indicates that the containing model class owns the reference.
+     */
+    OUTGOING,
+
+    /**
+     * Indicates that the containing model class is referenced by some other class.
+     */
+    INCOMING
+  }
+
+  /**
+   * The {@code KeepReferenceBuilder} enables the construction of new {@code
+   * KeepReferenceExpression} instances through a nice and fluent interface.
+   *
+   * @author Rico Bergmann
+   */
+  public static class KeepReferenceBuilder {
+
+    private ReferenceDirection referenceDirection;
+    private AttributePath attribute;
+    private ClassResource target;
+
+    @Nonnull
+    private List<KeepExpression> keeps;
+
+    /**
+     * Default constructor.
+     */
+    private KeepReferenceBuilder() {
+      this.keeps = new ArrayList<>();
+    }
+
+    /**
+     * Creates an outgoing reference for the given attribute.
+     */
+    @Nonnull
+    public KeepReferenceBuilder outgoing(@Nonnull AttributePath attribute) {
+      this.referenceDirection = ReferenceDirection.OUTGOING;
+      this.attribute = attribute;
+      return this;
+    }
+
+    /**
+     * Creates an incoming reference for the given attribute.
+     */
+    @Nonnull
+    public KeepReferenceBuilder incoming(@Nonnull AttributePath attribute) {
+      this.referenceDirection = ReferenceDirection.INCOMING;
+      this.attribute = attribute;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the attribute in the target join.
+     */
+    @Nonnull
+    public KeepReferenceBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Adds an attribute to the view for the referenced instances.
+     */
+    @Nonnull
+    public KeepReferenceBuilder keep(@Nonnull KeepExpression keepExpression) {
+      this.keeps.add(keepExpression);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process.
+     */
+    @Nonnull
+    public KeepReferenceExpression buildExpression() {
+      Assert.noNullArguments("All components must be specified", attribute, referenceDirection, target, keeps);
+      return new KeepReferenceExpression(attribute, referenceDirection, target, keeps);
+    }
+  }
+
+  /**
+   * Starts the creation process for a new {@code KeepReferenceExpression}.
+   */
+  @Nonnull
+  public static KeepReferenceBuilder keep() {
+    return new KeepReferenceBuilder();
+  }
+
+  @Nonnull
+  private final AttributePath attribute;
+
+  @Nonnull
+  private final ReferenceDirection referenceDirection;
+
+  @Nonnull
+  private final ClassResource target;
+
+  @Nonnull
+  private final List<KeepExpression> keeps;
+
+  /**
+   * Full constructor.
+   *
+   * @param attribute the attribute of the source model which contains the references
+   * @param referenceDirection whether the source model owns the reference or is being
+   *     referenced
+   * @param target the name of the class which enca
+   * @param keeps the statements which should be build the "nested" view for the references
+   */
+  public KeepReferenceExpression(
+      @Nonnull AttributePath attribute,
+      @Nonnull ReferenceDirection referenceDirection,
+      @Nonnull ClassResource target,
+      @Nonnull List<KeepExpression> keeps) {
+    this.attribute = attribute;
+    this.referenceDirection = referenceDirection;
+    this.target = target;
+    this.keeps = keeps;
+  }
+
+  /**
+   * Provides the attribute of the source model which contains the references.
+   */
+  @Nonnull
+  public AttributePath getAttribute() {
+    return attribute;
+  }
+
+  /**
+   * Provides the kind of reference, that is whether the containing Join owns the reference or is
+   * being referenced by another model class or Join.
+   */
+  @Nonnull
+  public ReferenceDirection getReferenceDirection() {
+    return referenceDirection;
+  }
+
+  /**
+   * Provides the name of the attribute under which the referenced instances should be made
+   * available.
+   */
+  @Nonnull
+  public ClassResource getTarget() {
+    return target;
+  }
+
+  /**
+   * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for
+   * the referenced instances.
+   */
+  @Nonnull
+  @Override
+  public List<KeepExpression> getKeeps() {
+    return new ArrayList<>(keeps);
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepReferenceExpression that = (KeepReferenceExpression) o;
+    return attribute.equals(that.attribute) && target.equals(that.target);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(attribute, target);
+  }
+
+  @Override
+  public String toString() {
+    String directionString = referenceDirection.toString().toLowerCase();
+    return "keep " + directionString + " " + attribute + " as type " + target;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..76e8d6027462a00d341906020ca57c638b1ed074
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSubTypeExpression.java
@@ -0,0 +1,167 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.util.Assert;
+
+/**
+ * A {@code KeepSubTypeExpression} instructs the {@code ModelJoin} runtime to instantiate the most
+ * specific type that inherits from this type as well. The additional instance will be built
+ * according to the {@code KeepExpression keep statements} that it is constructed of.
+ * <p>
+ * In order to prevent ambiguities a {@code KeepSubTypeExpression} may only be specified on joins
+ * that do not participate in another {@link JoinExpression join statement}.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepSubTypeExpression extends CompoundKeepExpression {
+
+  /**
+   * The {@code KeepSubTypeBuilder} enables the construction of new {@code KeepSubTypeExpression}
+   * instances through a nice and fluent interface.
+   *
+   * @author Rico Bergmann
+   */
+  public static class KeepSubTypeBuilder {
+
+    private ClassResource typeToKeep;
+    private ClassResource target;
+    private List<KeepExpression> keeps;
+
+    /**
+     * Default constructor.
+     */
+    private KeepSubTypeBuilder() {
+      this.keeps = new ArrayList<>();
+    }
+
+    /**
+     * Specifies the subtype that should be instantiated.
+     */
+    @Nonnull
+    public KeepSubTypeBuilder subtype(@Nonnull ClassResource type) {
+      this.typeToKeep = type;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the class that should be generated for the subtype instances.
+     */
+    @Nonnull
+    public KeepSubTypeBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Adds a {@link KeepExpression keep statement} for the subtype-class. The described attribute
+     * will be initialized for each instantiated subtype element.
+     */
+    @Nonnull
+    public KeepSubTypeBuilder keep(@Nonnull KeepExpression keep) {
+      this.keeps.add(keep);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process.
+     */
+    @Nonnull
+    public KeepSubTypeExpression buildExpression() {
+      Assert.noNullArguments("All components must be specified", typeToKeep, target, keeps);
+      return new KeepSubTypeExpression(typeToKeep, target, keeps);
+    }
+
+  }
+
+  /**
+   * Starts the creation process for a new {@code KeepSubTypeExpression}.
+   */
+  @Nonnull
+  public static KeepSubTypeBuilder keep() {
+    return new KeepSubTypeBuilder();
+  }
+
+  @Nonnull
+  private final ClassResource typeToKeep;
+
+  @Nonnull
+  private final ClassResource target;
+
+  @Nonnull
+  private final List<KeepExpression> keeps;
+
+  /**
+   * Full constructor.
+   *
+   * @param typeToKeep the subtype that should be instantiated.
+   * @param target the name of the view that should be generated for all matching instances
+   * @param keeps the keep statements that should form the attributes of the generated view
+   *     instances
+   */
+  public KeepSubTypeExpression(
+      @Nonnull ClassResource typeToKeep,
+      @Nonnull ClassResource target,
+      @Nonnull List<KeepExpression> keeps) {
+    this.typeToKeep = typeToKeep;
+    this.target = target;
+    this.keeps = keeps;
+  }
+
+  /**
+   * Provides the subtype that should be instantiated.
+   */
+  @Nonnull
+  public ClassResource getType() {
+    return typeToKeep;
+  }
+
+  /**
+   * Provides the name of the view that should be generated for all matching instances.
+   */
+  @Nonnull
+  public ClassResource getTarget() {
+    return target;
+  }
+
+  /**
+   * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for
+   * the instances of the subclass instances.
+   */
+  @Nonnull
+  @Override
+  public List<KeepExpression> getKeeps() {
+    return new ArrayList<>(keeps);
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepSubTypeExpression that = (KeepSubTypeExpression) o;
+    return typeToKeep.equals(that.typeToKeep) &&
+        target.equals(that.target);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(typeToKeep, target);
+  }
+
+  @Override
+  public String toString() {
+    return "keep subtype " + typeToKeep + " as " + target;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c72a3d9a42fdef32eafe621ceba232af271b803
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/KeepSuperTypeExpression.java
@@ -0,0 +1,166 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.util.Assert;
+
+// TODO add doc
+
+/**
+ * A {@code KeepSuperTypeExpression} instructs the {@code ModelJoin} runtime to instantiate the a
+ * supertype of some class in a join along an instance of that class. The additional instance will
+ * be built according to the {@code KeepExpression keep statements} that it is constructed of.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepSuperTypeExpression extends CompoundKeepExpression {
+
+  /**
+   * The {@code KeepSuperTypeBuilder} enables the construction of new {@code KeepSuperTypeExpression}
+   * instances through a nice and fluent interface.
+   *
+   * @author Rico Bergmann
+   */
+  public static class KeepSuperTypeBuilder {
+
+    private ClassResource typeToKeep;
+    private ClassResource target;
+    private List<KeepExpression> keeps;
+
+    /**
+     * Default constructor.
+     */
+    private KeepSuperTypeBuilder() {
+      this.keeps = new ArrayList<>();
+    }
+
+    /**
+     * Specifies the supertype that should be instantiated.
+     */
+    @Nonnull
+    public KeepSuperTypeBuilder supertype(@Nonnull ClassResource type) {
+      this.typeToKeep = type;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the class that should be generated for the subtype instances.
+     */
+    @Nonnull
+    public KeepSuperTypeBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Adds a {@link KeepExpression keep statement} for the subtype-class. The described attribute
+     * will be initialized for each instantiated subtype element.
+     */
+    @Nonnull
+    public KeepSuperTypeBuilder keep(@Nonnull KeepExpression keep) {
+      this.keeps.add(keep);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process.
+     */
+    @Nonnull
+    public KeepSuperTypeExpression buildExpression() {
+      Assert.noNullArguments("All components must be specified", typeToKeep, target, keeps);
+      return new KeepSuperTypeExpression(typeToKeep, target, keeps);
+    }
+
+  }
+
+  /**
+   * Starts the creation process for a new {@code KeepSuperTypeExpression}.
+   */
+  @Nonnull
+  public static KeepSuperTypeBuilder keep() {
+    return new KeepSuperTypeBuilder();
+  }
+
+  @Nonnull
+  private final ClassResource typeToKeep;
+
+  @Nonnull
+  private final ClassResource target;
+
+  @Nonnull
+  private final List<KeepExpression> keeps;
+
+  /**
+   * Full constructor.
+   *
+   * @param typeToKeep the supertype that should be instantiated.
+   * @param target the name of the view that should be generated for all matching instances
+   * @param keeps the keep statements that should form the attributes of the generated view
+   *     instances
+   */
+  public KeepSuperTypeExpression(
+      @Nonnull ClassResource typeToKeep,
+      @Nonnull ClassResource target,
+      @Nonnull List<KeepExpression> keeps) {
+    this.typeToKeep = typeToKeep;
+    this.target = target;
+    this.keeps = keeps;
+  }
+
+  /**
+   * Provides the supertype that should be instantiated.
+   */
+  @Nonnull
+  public ClassResource getType() {
+    return typeToKeep;
+  }
+
+  /**
+   * Provides the name of the view that should be generated for all matching instances.
+   */
+  @Nonnull
+  public ClassResource getTarget() {
+    return target;
+  }
+
+  /**
+   * Provides all {@code KeepExpression keep expressions} that should be used to build the Join for
+   * the instances of the superclass instances.
+   */
+  @Nonnull
+  @Override
+  public List<KeepExpression> getKeeps() {
+    return new ArrayList<>(keeps);
+  }
+
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    KeepSuperTypeExpression that = (KeepSuperTypeExpression) o;
+    return typeToKeep.equals(that.typeToKeep) &&
+        target.equals(that.target);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(typeToKeep, target);
+  }
+
+  @Override
+  public String toString() {
+    return "keep supertype " + typeToKeep + " as " + target;
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..30382b532ada48b163756519650e01c25516fc3a
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ModelJoinExpression.java
@@ -0,0 +1,102 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nonnull;
+
+/**
+ * The {@code ModelJoinExpression} represents an actual ModelJoin, i.e. a specification of which
+ * joins to use and how they should be built.
+ *
+ * @author Rico Bergmann
+ */
+public class ModelJoinExpression implements Iterable<JoinExpression> {
+	
+  private String name = "";
+  
+  public String getName() {
+	  return name;
+  }
+  
+  public void setName(String n) {
+	  name = n;
+  }
+
+  /**
+   * Creates a new {@code ModelJoin}.
+   *
+   * @param joins the joins that should be included.
+   */
+  @Nonnull
+  public static ModelJoinExpression with(@Nonnull List<JoinExpression> joins) {
+    return new ModelJoinExpression(joins);
+  }
+
+  /**
+   * Ensures that all joins specify distinct join targets and throws an {@code
+   * IllegalArgumentException} otherwise.
+   */
+  private static void assertDifferentTargetsForAllJoins(Collection<JoinExpression> joinsToCheck) {
+    final long totalNumberOfJoins = joinsToCheck.size();
+    final long numberOfDistinctTargetsSpecified = joinsToCheck.stream()//
+        .map(JoinExpression::getTarget) //
+        .distinct() //
+        .count();
+
+    if (numberOfDistinctTargetsSpecified < totalNumberOfJoins) {
+      throw new IllegalArgumentException("Some joins share the same target");
+    }
+  }
+
+  @Nonnull
+  private final List<JoinExpression> joins;
+
+  /**
+   * Full constructor.
+   *
+   * @param joins the joins that this ModelJoin consists of
+   */
+  public ModelJoinExpression(List<JoinExpression> joins) {
+    assertDifferentTargetsForAllJoins(joins);
+    this.joins = joins;
+  }
+
+  /**
+   * Provides all joins {@code this} model specifies.
+   */
+  @Nonnull
+  public Collection<JoinExpression> getJoins() {
+    return new ArrayList<>(joins);
+  }
+
+  @Nonnull
+  @Override
+  public Iterator<JoinExpression> iterator() {
+    return joins.iterator();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    ModelJoinExpression that = (ModelJoinExpression) o;
+    return joins.equals(that.joins);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(joins);
+  }
+
+  @Override
+  public String toString() {
+    return joins.toString();
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c3d7b4d9aa8a374d367bf5d3ba6948531538b9c
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/NaturalJoinExpression.java
@@ -0,0 +1,96 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+
+/**
+ * The {@code natural join} combines model classes based on attributes of the
+ * same name and type.
+ * <p>
+ * As an example, consider the following two classes:
+ * 
+ * <pre>
+ * {
+ * 	&#64;code
+ * 	class Person {
+ * 		String firstname;
+ * 		String lastname;
+ * 		Date dateOfBirth;
+ * 		Char gender;
+ * 		// ... other code omitted
+ * 	}
+ *
+ * 	class Employee {
+ * 		int id;
+ * 		String firstname;
+ * 		String lastname;
+ * 		Date dateOfBirth;
+ * 		int salary;
+ * 		// ... other code omitted
+ * 	}
+ * }
+ * </pre>
+ * <p>
+ * Performing a natural join on {@code Person} and {@code Employee} will combine
+ * instances only if they share the same first name, last name and date of
+ * birth. All other fields will not be considered during the comparison.
+ * 
+ * <pre>
+ * ┌───────────────────┐             ┌───────────────────┐
+ * │ Person            │             │ Employee          │
+ * ├───────────────────┤             ├───────────────────┤
+ * │firstname: String  │ ┄┄┄┄┄┄┄╮    │id: int            │
+ * │lastname: String   │ ┄┄┄┄┄╮ ╰┄┄┄ │firstname: String  │
+ * │dateOfBirth: Date  │ ┄┄┄╮ ╰┄┄┄┄┄ │lastname: String   │
+ * │gender: Char       │    ╰┄┄┄┄┄┄┄ │dateOfBirth: Date  │
+ * ├───────────────────┤             │salary: int        │
+ * │ ...               │             ├───────────────────┤
+ * └───────────────────┘             │ ...               │
+ *                                   └───────────────────┘
+ * </pre>
+ * <p>
+ * The resulting class will by default only contain the attributes that where
+ * used in comparison (first name, last name and date of birth in the example).
+ *
+ * @author Rico Bergmann
+ */
+public class NaturalJoinExpression extends JoinExpression {
+
+	/*
+	 * TODO: discuss should the natural join extend theta join instead (from a
+	 * theoretical view more appropriate. However this would also mean that the
+	 * natural join has to derive the join condition as well. This in turn would
+	 * also meant that class resources would have to know which attributes are
+	 * defined on them.
+	 */
+
+	/**
+	 * Full constructor.
+	 *
+	 * @param left
+	 *            the left class to use in the join
+	 * @param right
+	 *            the right class to use in the join
+	 * @param target
+	 *            the name of the resulting view
+	 * @param keeps
+	 *            the keep statements in the join
+	 */
+	public NaturalJoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right,
+			@Nonnull ClassResource target, @Nonnull List<KeepExpression> keeps) {
+		super(left, right, target, keeps);
+	}
+
+	@Nonnull
+	@Override
+	public JoinType getType() {
+		return JoinType.NATURAL;
+	}
+
+	@Nonnull
+	@Override
+	String getJoinTypeAsString() {
+		return "natural join";
+	}
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..376dad0da9717dea91041d9951f1febe2e0de01c
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/OuterJoinExpression.java
@@ -0,0 +1,150 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+
+/**
+ * The {@code outer join} is a special kind of {@code natural join}: depending
+ * on its specific type, all elements of one source will definitely be included
+ * - potentially being joined with matching instances of the other source.
+ * <p>
+ * Matches will be determined the same way as with a normal
+ * {@code natural join}. However, depending on the kind of join, the result will
+ * include all instances of the {@code left} or {@code right} source model, no
+ * matter whether they have a matching instance in the other model or not. In
+ * case that some instance has no corresponding instance in the other model, all
+ * fields that belong to the other model will be set to {@code null}.
+ * <p>
+ * Consider the following example (derived from the model shown in the
+ * {@link NaturalJoinExpression} documentation)
+ * 
+ * <pre>
+ * Person:
+ * ┌───────────┬──────────┬─────────────┬────────┐
+ * │ firstname │ lastname │ dateOfBirth │ gender │
+ * ├───────────┼──────────┼─────────────┼────────┤
+ * │ Jane      │ Doe      │ 1999-01-01  │   f    │
+ * │ John      │ Doe      │ 1998-02-03  │   m    │
+ * │ Maria     │ Miller   │ 1990-12-31  │   f    │
+ * └───────────┴──────────┴─────────────┴────────┘
+ *
+ * Employee:
+ * ┌────┬───────────┬──────────┬─────────────┬────────┐
+ * │ id │ firstname │ lastname │ dateOfBirth │ salary │
+ * ├────┼───────────┼──────────┼─────────────┼────────┤
+ * │ 1  │ Jane      │ Doe      │ 1999-01-01  │ 20000  │
+ * │ 2  │ John      │ Doe      │ 1997-03-04  │ 15000  │
+ * │ 3  │ Maria     │ Miller   │ 1990-12-31  │ 17500  │
+ * └────┴───────────┴──────────┴─────────────┴────────┘
+ * </pre>
+ * 
+ * Performing an left outer join as described in the
+ * {@link NaturalJoinExpression natural join} documentation will lead to the
+ * following result (again written in relational style):
+ * 
+ * <pre>
+ * ┌───────────┬──────────┬─────────────┬────────┬──────┬────────┐
+ * │ firstname │ lastname │ dateOfBirth │ gender │ id   │ salary │
+ * ├───────────┼──────────┼─────────────┼────────┼──────┼────────┤
+ * │ Jane      │ Doe      │ 1999-01-01  │   f    │ 1    │ 20000  │
+ * │ John      │ Doe      │ 1998-02-03  │   m    │ NULL │ NULL   │
+ * │ Maria     │ Miller   │ 1990-12-31  │   f    │ 3    │ 17500  │
+ * └───────────┴──────────┴─────────────┴────────┴──────┴────────┘
+ * </pre>
+ *
+ * @author Rico Bergmann
+ */
+public class OuterJoinExpression extends NaturalJoinExpression {
+
+	/**
+	 * The {@code JoinDirection} denotes which side of the source models should
+	 * keep all of its instances - no matter whether they have a matching
+	 * instances in the other model or not.
+	 */
+	public enum JoinDirection {
+		LEFT, RIGHT
+	}
+
+	@Nonnull
+	private final JoinDirection direction;
+
+	/**
+	 * Full constructor.
+	 *
+	 * @param left
+	 *            the left class to use in the join
+	 * @param right
+	 *            the right class to use in the join
+	 * @param target
+	 *            the name of the resulting view
+	 * @param direction
+	 *            the source model which should definitely be kept
+	 * @param keeps
+	 *            the keep statements in the join
+	 */
+	public OuterJoinExpression(@Nonnull ClassResource left, @Nonnull ClassResource right, @Nonnull ClassResource target,
+			@Nonnull JoinDirection direction, @Nonnull List<KeepExpression> keeps) {
+		super(left, right, target, keeps);
+		this.direction = direction;
+	}
+
+	/**
+	 * Provides the position of the source model on which the join should be
+	 * based.
+	 */
+	@Nonnull
+	public JoinDirection getDirection() {
+		return direction;
+	}
+
+	/**
+	 * Provides the source model on which the join should be based.
+	 */
+	@Override
+	public ClassResource getBaseModel() {
+		switch (direction) {
+		case LEFT:
+			return left;
+		case RIGHT:
+			return right;
+		default:
+			throw new AssertionError(direction);
+		}
+	}
+
+	@Override
+	public ClassResource getOtherModel() {
+		switch (direction) {
+		case LEFT:
+			return right;
+		case RIGHT:
+			return left;
+		default:
+			throw new AssertionError(direction);
+		}
+	}
+
+	@Nonnull
+	@Override
+	public JoinType getType() {
+		return JoinType.OUTER;
+	}
+
+	@Nonnull
+	@Override
+	String getJoinTypeAsString() {
+		String directionString;
+		switch (this.direction) {
+		case LEFT:
+			directionString = "left";
+			break;
+		case RIGHT:
+			directionString = "right";
+			break;
+		default:
+			throw new AssertionError(this.direction);
+		}
+		return directionString + " outer join";
+	}
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java
new file mode 100644
index 0000000000000000000000000000000000000000..39f777b05b511d0248c0c84a47535c05470dc892
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/grammar/ThetaJoinExpression.java
@@ -0,0 +1,77 @@
+package org.rosi_project.model_sync.model_join.representation.grammar;
+
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint;
+
+/**
+ * A {@code theta join} is the most general type of join. It will determine join-compatibility
+ * based on an arbitrary predicate which may evaluate to {@code true} or {@code false}, denoting
+ * whether two instances are to be joined or not.
+ * <p>
+ * To specify this condition, {@link OCLConstraint OCL} is used.
+ * <p>
+ * As a simple example, consider the following two classes:
+ * <pre>
+ * ┌───────────────────┐     ┌───────────────────┐
+ * │ Person            │     │ Employee          │
+ * ├───────────────────┤     ├───────────────────┤
+ * │firstname: String  │     │id: int            │
+ * │lastname: String   │     │fullname: String   │
+ * │dateOfBirth: Date  │     │dateOfBirth: Date  │
+ * │gender: Char       │     │salary: int        │
+ * ├───────────────────┤     ├───────────────────┤
+ * │ ...               │     │ ...               │
+ * └───────────────────┘     └───────────────────┘
+ * </pre>
+ * <p>
+ * The expression {@code person.firstname + ' ' + person.lastname = employee.fullname} would than
+ * match all persons with employee instances if they have the same name.
+ *
+ * @author Rico Bergmann
+ */
+public class ThetaJoinExpression extends JoinExpression {
+
+  @Nonnull
+  private final OCLConstraint condition;
+
+  /**
+   * Full constructor.
+   *
+   * @param left the left class to use in the join
+   * @param right the right class to use in the join
+   * @param target the name of the resulting view
+   * @param condition the condition which should be used to determine join-compatibility
+   */
+  public ThetaJoinExpression(
+      @Nonnull ClassResource left,
+      @Nonnull ClassResource right,
+      @Nonnull ClassResource target,
+      @Nonnull OCLConstraint condition,
+      @Nonnull List<KeepExpression> keeps) {
+    super(left, right, target, keeps);
+    this.condition = condition;
+  }
+
+  /**
+   * Provides the condition which should be used to determine whether two instances are join
+   * compatible or not.
+   */
+  @Nonnull
+  public OCLConstraint getCondition() {
+    return condition;
+  }
+
+  @Nonnull
+  @Override
+  public JoinType getType() {
+    return JoinType.THETA;
+  }
+
+  @Nonnull
+  @Override
+  String getJoinTypeAsString() {
+    return "theta join";
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..87c6f0cc6383feb4e067c767e08bd12c303d308e
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParser.java
@@ -0,0 +1,32 @@
+package org.rosi_project.model_sync.model_join.representation.parser;
+
+import java.io.File;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+/**
+ * The {@code ModelJoinParser} reads a {@code ModelJoin} file from disk and creates a matching
+ * logical representation for it.
+ *
+ * @author Rico Bergmann
+ */
+public interface ModelJoinParser {
+
+  /**
+   * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}, wrapped into an {@code
+   * Optional}. If parsing fails, an empty {@code Optional} will be returned.
+   */
+  @Nonnull
+  Optional<ModelJoinExpression> read(@Nonnull File modelFile);
+
+  /**
+   * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}. If parsing fails, a
+   * {@link ModelJoinParsingException} will be thrown.
+   *
+   * @throws ModelJoinParsingException if the model may not be parsed for some reason.
+   */
+  @Nonnull
+  ModelJoinExpression readOrThrow(@Nonnull File modelFile);
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b72c01ee5d4926b87db1cfa80bc9839e385d104
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java
@@ -0,0 +1,31 @@
+package org.rosi_project.model_sync.model_join.representation.parser;
+
+/**
+ * The {@code ModelJoinParsingException} indicates that a {@code ModelJoin} file could not be read
+ * correctly, e.g. because of missing values or a malformed structure.
+ *
+ * @author Rico Bergmann
+ */
+public class ModelJoinParsingException extends RuntimeException {
+
+  /**
+   * @see RuntimeException#RuntimeException(String)
+   */
+  public ModelJoinParsingException(String message) {
+    super(message);
+  }
+
+  /**
+   * @see RuntimeException#RuntimeException(String, Throwable)
+   */
+  public ModelJoinParsingException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  /**
+   * @see RuntimeException#RuntimeException(Throwable)
+   */
+  public ModelJoinParsingException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..86773781a112c27a910fca63acc710f9d50f331f
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java
@@ -0,0 +1,104 @@
+package org.rosi_project.model_sync.model_join.representation.parser.antlr;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.TokenStream;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParser;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinLexer;
+
+/**
+ * The {@code AntlrBackedModelJoinParser} reads a {@code ModelJoin} file according to a compiled
+ * {@code ANTLR} grammar and generates the matching {@link ModelJoinExpression} for the file.
+ *
+ * @author Rico Bergmann
+ * @see <a href="https://www.antlr.org/">ANTLR project</a>
+ */
+public class AntlrBackedModelJoinParser implements ModelJoinParser {
+
+  @Nonnull
+  @Override
+  public Optional<ModelJoinExpression> read(@Nonnull File modelFile) {
+    try {
+      return Optional.of(invokeAntlr(modelFile));
+    } catch (ModelJoinParsingException e) {
+      return Optional.empty();
+    }
+  }
+
+  @Nonnull
+  @Override
+  public ModelJoinExpression readOrThrow(@Nonnull File modelFile) {
+    return invokeAntlr(modelFile);
+  }
+
+  /**
+   * Executes the whole ANTLR workflow.
+   *
+   * @throws ModelJoinParsingException if the workflow fails. Details on the error will be added
+   *     to the exception.
+   */
+  protected ModelJoinExpression invokeAntlr(@Nonnull File modelFile) {
+    CharStream modelStream = consumeCompleteModelFileContent(modelFile);
+    ModelJoinLexer lexer = initializeLexerBasedOn(modelStream);
+    TokenStream tokenStream = tokenize(lexer);
+
+    org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. //
+        ModelJoinParser modelJoinParser = initializeParserBasedOn(tokenStream);
+
+    org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. //
+        ModelJoinParser.ModeljoinContext modelJoinContext = modelJoinParser.modeljoin();
+
+    ModelJoinVisitor modelJoinVisitor = new ModelJoinVisitor();
+
+    return modelJoinVisitor.visitModeljoin(modelJoinContext);
+  }
+
+  /**
+   * Converts the content of a {@code ModelJoin} file into an ANTLR input stream.
+   *
+   * @throws ModelJoinParsingException if the file may not be read
+   */
+  @Nonnull
+  protected CharStream consumeCompleteModelFileContent(@Nonnull File modelFile) {
+    try {
+      return CharStreams.fromPath(modelFile.toPath());
+    } catch (IOException e) {
+      throw new ModelJoinParsingException(e);
+    }
+  }
+
+  /**
+   * Sets up a lexer for the given {@code ModelJoin} file.
+   */
+  @Nonnull
+  protected ModelJoinLexer initializeLexerBasedOn(@Nonnull CharStream modelFile) {
+    return new ModelJoinLexer(modelFile);
+  }
+
+  /**
+   * Executes the lexing step.
+   */
+  @Nonnull
+  protected TokenStream tokenize(@Nonnull ModelJoinLexer lexer) {
+    return new CommonTokenStream(lexer);
+  }
+
+  /**
+   * Sets up the parser.
+   */
+  @Nonnull
+  protected org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. //
+      ModelJoinParser initializeParserBasedOn(@Nonnull TokenStream tokens) {
+    return new org.rosi_project.model_sync.model_join.representation.parser.antlr.generated. //
+        ModelJoinParser(tokens);
+  }
+
+}
+
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fe1c7bd180e24161204ccab4a59d2a557985536
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java
@@ -0,0 +1,183 @@
+package org.rosi_project.model_sync.model_join.representation.parser.antlr;
+
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression.JoinType;
+import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.JoinContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.NaturaljoinContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.OuterjoinContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.ThetajoinContext;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.OuterJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder;
+
+/**
+ * The {@code JoinStatementVisitor} creates {@link JoinExpression} instances for a parsed {@code
+ * ModelJoin} file.
+ *
+ * @author Rico Bergmann
+ */
+public class JoinStatementVisitor extends ModelJoinBaseVisitor<JoinExpression> {
+
+  private final KeepStatementVisitor keepStatementVisitor;
+
+  /**
+   * Default constructor. Nothing special about it.
+   */
+  JoinStatementVisitor() {
+    this.keepStatementVisitor = new KeepStatementVisitor();
+  }
+
+  @Override
+  public JoinExpression visitJoin(JoinContext ctx) {
+    JoinType joinType = determineJoinType(ctx);
+    return invokeJoinBuilderAccordingTo(joinType, ctx);
+  }
+
+  /**
+   * Checks, which kind of join is specified in a specific {@code JoinContext}.
+   */
+  private JoinType determineJoinType(@Nonnull JoinContext ctx) {
+    if (ctx.thetajoin() != null) {
+      return JoinType.THETA;
+    } else if (ctx.naturaljoin() != null) {
+      return JoinType.NATURAL;
+    } else if (ctx.outerjoin() != null) {
+      return JoinType.OUTER;
+    } else {
+      throw new ModelJoinParsingException("Unkown join type in join " + ctx);
+    }
+  }
+
+  /**
+   * Generates a {@link JoinExpression} according to the {@link JoinType} of the given {@code
+   * JoinContext}.
+   */
+  private JoinExpression invokeJoinBuilderAccordingTo(@Nonnull JoinType type,
+      @Nonnull JoinContext ctx) {
+    switch (type) {
+      case THETA:
+        return generateThetaJoin(ctx);
+      case NATURAL:
+        return generateNaturalJoin(ctx);
+      case OUTER:
+        return generateOuterJoin(ctx);
+      default:
+        throw new AssertionError(type);
+    }
+  }
+
+  /**
+   * Creates the {@link ThetaJoinExpression} instance contained in a {@code JoinContext}. This
+   * method assumes that the provided {@code ctx} indeed contains a theta join.
+   */
+  private ThetaJoinExpression generateThetaJoin(@Nonnull JoinContext ctx) {
+    ThetajoinContext thetajoinContext = ctx.thetajoin();
+
+    ClassResource left = ClassResource.fromQualifiedName(thetajoinContext.classres(0).getText());
+    ClassResource right = ClassResource.fromQualifiedName(thetajoinContext.classres(1).getText());
+
+    ThetaJoinBuilder joinBuilder = JoinFactory.createNew()
+        .theta()
+        .join(left)
+        .with(right)
+        .as(ClassResource.fromQualifiedName(ctx.classres().getText()))
+        .where(OCLConstraint.of(thetajoinContext.oclcond().getText()));
+
+    ctx.keepaggregatesexpr().stream()
+        .map(keepStatementVisitor::visitKeepaggregatesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepattributesexpr().stream()
+        .map(keepStatementVisitor::visitKeepattributesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepcalculatedexpr().stream()
+        .map(keepStatementVisitor::visitKeepcalculatedexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepexpr().stream()
+        .map(keepStatementVisitor::visitKeepexpr)
+        .forEach(joinBuilder::keep);
+
+    return joinBuilder.done();
+  }
+
+  /**
+   * Creates the {@link NaturalJoinExpression} instance contained in a {@code JoinContext}. This
+   * method assumes that the provided {@code ctx} indeed contains a natural join.
+   */
+  private NaturalJoinExpression generateNaturalJoin(@Nonnull JoinContext ctx) {
+    NaturaljoinContext naturalJoinContext = ctx.naturaljoin();
+
+    ClassResource left = ClassResource.fromQualifiedName(naturalJoinContext.classres(0).getText());
+    ClassResource right = ClassResource.fromQualifiedName(naturalJoinContext.classres(1).getText());
+
+    NaturalJoinBuilder joinBuilder = JoinFactory.createNew()
+        .natural()
+        .join(left)
+        .with(right)
+        .as(ClassResource.fromQualifiedName(ctx.classres().getText()));
+
+    ctx.keepaggregatesexpr().stream()
+        .map(keepStatementVisitor::visitKeepaggregatesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepattributesexpr().stream()
+        .map(keepStatementVisitor::visitKeepattributesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepcalculatedexpr().stream()
+        .map(keepStatementVisitor::visitKeepcalculatedexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepexpr().stream()
+        .map(keepStatementVisitor::visitKeepexpr)
+        .forEach(joinBuilder::keep);
+
+    return joinBuilder.done();
+  }
+
+  /**
+   * Creates the {@link OuterJoinExpression} instance contained in a {@code JoinContext}. This
+   * method assumes that the provided {@code ctx} indeed contains an outer join.
+   */
+  private OuterJoinExpression generateOuterJoin(@Nonnull JoinContext ctx) {
+    OuterjoinContext outerJoinContext = ctx.outerjoin();
+
+    ClassResource left = ClassResource.fromQualifiedName(outerJoinContext.classres(0).getText());
+    ClassResource right = ClassResource.fromQualifiedName(outerJoinContext.classres(1).getText());
+
+    OuterJoinBuilder joinBuilder = JoinFactory.createNew()
+        .outer()
+        .join(left)
+        .with(right)
+        .as(ClassResource.fromQualifiedName(ctx.classres().getText()));
+
+    if (outerJoinContext.leftouterjoin() != null) {
+      joinBuilder.leftOuter();
+    } else if (outerJoinContext.rightouterjoin() != null) {
+      joinBuilder.rightOuter();
+    } else {
+      throw new ModelJoinParsingException("Expected either 'left' or 'right' 'outer join'");
+    }
+
+    ctx.keepaggregatesexpr().stream()
+        .map(keepStatementVisitor::visitKeepaggregatesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepattributesexpr().stream()
+        .map(keepStatementVisitor::visitKeepattributesexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepcalculatedexpr().stream()
+        .map(keepStatementVisitor::visitKeepcalculatedexpr)
+        .forEach(joinBuilder::keep);
+    ctx.keepexpr().stream()
+        .map(keepStatementVisitor::visitKeepexpr)
+        .forEach(joinBuilder::keep);
+
+    return joinBuilder.done();
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e9789d220ded97f3b053e9c1a461ecd29f9d022
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java
@@ -0,0 +1,307 @@
+package org.rosi_project.model_sync.model_join.representation.parser.antlr;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.core.OCLStatement;
+import org.rosi_project.model_sync.model_join.representation.core.RelativeAttribute;
+import org.rosi_project.model_sync.model_join.representation.core.TypedAttributePath;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression.AggregationType;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepCalculatedExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSubTypeExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.AttrresContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepaggregatesexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepattributesexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepcalculatedexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepincomingexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepoutgoingexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepsubtypeexprContext;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.KeepsupertypeexprContext;
+
+/**
+ * The {@code KeepStatementVisitor} creates {@link KeepExpression} instances from a parsed {@code
+ * ModelJoin} file.
+ *
+ * @author Rico Bergmann
+ */
+public class KeepStatementVisitor extends ModelJoinBaseVisitor<KeepExpression> {
+
+  /**
+   * A simple enumeration of the possible keep statements for compound statements as defined in the
+   * {@code ModelJoin} ANTLR grammar.
+   */
+  private enum KeepExprType {
+
+    /**
+     * The {@code KEEP_TYPE_EXPR} subsumes both {@code keep subtype} as well as {@code keep
+     * supertype} expressions.
+     */
+    KEEP_TYPE_EXPR,
+
+    /**
+     * The {@code KEEP_INCOMING_EXPR} corresponds to a {@code keep incoming reference} statement.
+     */
+    KEEP_INCOMING_EXPR,
+
+    /**
+     * The {@code KEEP_OUTGOING_EXPR} corresponds to a {@code keep outgoing reference} statement.
+     */
+    KEEP_OUTGOING_EXPR
+
+  }
+
+  @Override
+  public KeepExpression visitKeepattributesexpr(KeepattributesexprContext ctx) {
+    return new KeepAttributesExpression(
+        ctx.attrres().stream()
+            .map(AttrresContext::getText)
+            .map(AttributePath::fromQualifiedPath)
+            .collect(ArrayList::new, ArrayList::add, ArrayList::addAll)
+    );
+
+  }
+
+  @Override
+  public KeepExpression visitKeepaggregatesexpr(KeepaggregatesexprContext ctx) {
+    String rawAggregationType = ctx.aggrtype().getText();
+    AggregationType aggregationType = AggregationType.valueOf(rawAggregationType.toUpperCase());
+
+    return new KeepAggregateExpression(
+        aggregationType /* SUM / MAX / MIN etc */,
+        RelativeAttribute.of(ctx.relattr().getText()) /* the attribute in the target relation  */,
+        AttributePath.fromQualifiedPath(ctx.classres().getText()) /* the source attribute */,
+        AttributePath.fromQualifiedPath(ctx.attrres().getText()) /* name in the new ModelJoin */
+    );
+  }
+
+  @Override
+  public KeepExpression visitKeepcalculatedexpr(KeepcalculatedexprContext ctx) {
+    OCLStatement oclExpr = OCLStatement.of(ctx.oclcond().getText());
+
+    return KeepCalculatedExpression //
+        .keepCalculatedAttribute(oclExpr) //
+        .as(TypedAttributePath.fromQualifiedTypedPath(ctx.typedattrres().getText())
+        );
+  }
+
+  @Override
+  public KeepExpression visitKeepexpr(KeepexprContext ctx) {
+    KeepExprType keepExprType = determineExpressionTypeFor(ctx);
+
+    switch (keepExprType) {
+      case KEEP_TYPE_EXPR:
+        return buildKeepTypeExpressionFor(ctx);
+      case KEEP_INCOMING_EXPR:
+        return buildKeepIncomingReferenceExpressionFor(ctx);
+      case KEEP_OUTGOING_EXPR:
+        return buildKeepOutgoingReferenceExpressionFor(ctx);
+      default:
+        throw new AssertionError(keepExprType);
+    }
+  }
+
+  /**
+   * Checks, which kind of expression is specified in a specific {@code KeepexprContext}.
+   */
+  private KeepExprType determineExpressionTypeFor(KeepexprContext ctx) {
+
+    /*
+     * From the ANTLR grammar we know that exactly one of the IF cases has to hold
+     */
+    if (ctx.keeptypeexpr() != null) {
+      return KeepExprType.KEEP_TYPE_EXPR;
+    } else if (ctx.keepincomingexpr() != null) {
+      return KeepExprType.KEEP_INCOMING_EXPR;
+    } else if (ctx.keepoutgoingexpr() != null) {
+      return KeepExprType.KEEP_OUTGOING_EXPR;
+    } else {
+      throw new ModelJoinParsingException(
+          "Expected either 'keep reference' or 'keep type'  expression: " + ctx);
+    }
+  }
+
+  /**
+   * Creates the appropriate {@link KeepExpression} for a {@code KeepexprContext}.
+   */
+  private KeepExpression buildKeepTypeExpressionFor(KeepexprContext ctx) {
+    if (ctx.keeptypeexpr().keepsubtypeexpr() != null) {
+      return buildKeepSubtypeExpressionFor(ctx);
+    } else if (ctx.keeptypeexpr().keepsupertypeexpr() != null) {
+      return buildKeepSupertypeExpressionFor(ctx);
+    } else {
+      throw new ModelJoinParsingException(
+          "Expected either 'keep supertype' or 'keep subtype'  expression: " + ctx);
+    }
+  }
+
+  /**
+   * Creates the {@link KeepReferenceExpression} instance contained in a {@code KeepexprContext}.
+   * This method assumes that the provided {@code ctx} indeed contains a keep incoming reference
+   * statement.
+   */
+  private KeepReferenceExpression buildKeepIncomingReferenceExpressionFor(KeepexprContext ctx) {
+    KeepincomingexprContext incomingCtx = ctx.keepincomingexpr();
+
+    AttributePath attributeToKeep = AttributePath
+        .fromQualifiedPath(incomingCtx.attrres().getText());
+
+    List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream()
+        .map(this::visitKeepattributesexpr)
+        .collect(Collectors.toList());
+
+    nestedExpressions.addAll(ctx.keepaggregatesexpr().stream()
+        .map(this::visitKeepaggregatesexpr)
+        .collect(Collectors.toList())
+    );
+
+    nestedExpressions.addAll(ctx.keepexpr().stream()
+        .map(this::visitKeepexpr)
+        .collect(Collectors.toList())
+    );
+
+    KeepReferenceExpression.KeepReferenceBuilder expressionBuilder = KeepReferenceExpression.keep()
+        .incoming(attributeToKeep);
+
+    ClassResource targetClass;
+    if (incomingCtx.classres() != null) {
+      targetClass = ClassResource.fromQualifiedName(incomingCtx.classres().getText());
+    } else {
+      targetClass = attributeToKeep.getContainingClass();
+    }
+
+    expressionBuilder.as(targetClass);
+    nestedExpressions.forEach(expressionBuilder::keep);
+
+    return expressionBuilder.buildExpression();
+  }
+
+  /**
+   * Creates the {@link KeepReferenceExpression} instance contained in a {@code KeepexprContext}.
+   * This method assumes that the provided {@code ctx} indeed contains a keep outgoing reference
+   * statement.
+   */
+  private KeepReferenceExpression buildKeepOutgoingReferenceExpressionFor(KeepexprContext ctx) {
+    KeepoutgoingexprContext outgoingCtx = ctx.keepoutgoingexpr();
+
+    AttributePath attributeToKeep = AttributePath
+        .fromQualifiedPath(outgoingCtx.attrres().getText());
+
+    List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream()
+        .map(this::visitKeepattributesexpr)
+        .collect(Collectors.toList());
+
+    nestedExpressions.addAll(ctx.keepaggregatesexpr().stream()
+        .map(this::visitKeepaggregatesexpr)
+        .collect(Collectors.toList())
+    );
+
+    nestedExpressions.addAll(ctx.keepexpr().stream()
+        .map(this::visitKeepexpr)
+        .collect(Collectors.toList())
+    );
+
+    KeepReferenceExpression.KeepReferenceBuilder expressionBuilder = KeepReferenceExpression.keep()
+        .outgoing(attributeToKeep);
+
+    ClassResource targetClass;
+    if (outgoingCtx.classres() != null) {
+      targetClass = ClassResource.fromQualifiedName(outgoingCtx.classres().getText());
+    } else {
+      targetClass = attributeToKeep.getContainingClass();
+    }
+
+    expressionBuilder.as(targetClass);
+    nestedExpressions.forEach(expressionBuilder::keep);
+
+    return expressionBuilder.buildExpression();
+  }
+
+  /**
+   * Creates the {@link KeepSubTypeExpression} instance contained in a {@code KeepexprContext}. This
+   * method assumes that the provided {@code ctx} indeed contains a keep subtype statement.
+   */
+  private KeepSubTypeExpression buildKeepSubtypeExpressionFor(KeepexprContext ctx) {
+    KeepsubtypeexprContext subtypeCtx = ctx.keeptypeexpr().keepsubtypeexpr();
+
+    ClassResource typeToKeep = ClassResource.fromQualifiedName(subtypeCtx.classres(0).getText());
+
+    List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream()
+        .map(this::visitKeepattributesexpr)
+        .collect(Collectors.toList());
+
+    nestedExpressions.addAll(ctx.keepaggregatesexpr().stream()
+        .map(this::visitKeepaggregatesexpr)
+        .collect(Collectors.toList())
+    );
+
+    nestedExpressions.addAll(ctx.keepexpr().stream()
+        .map(this::visitKeepexpr)
+        .collect(Collectors.toList())
+    );
+
+    KeepSubTypeExpression.KeepSubTypeBuilder expressionBuilder = KeepSubTypeExpression.keep()
+        .subtype(typeToKeep);
+
+    ClassResource targetClass;
+    if (subtypeCtx.classres() != null) {
+      targetClass = ClassResource.fromQualifiedName(subtypeCtx.classres(1).getText());
+    } else {
+      targetClass = typeToKeep;
+    }
+
+    expressionBuilder.as(targetClass);
+    nestedExpressions.forEach(expressionBuilder::keep);
+
+    return expressionBuilder.buildExpression();
+  }
+
+  /**
+   * Creates the {@link KeepSuperTypeExpression} instance contained in a {@code KeepexprContext}.
+   * This method assumes that the provided {@code ctx} indeed contains a keep supertype statement.
+   */
+  private KeepSuperTypeExpression buildKeepSupertypeExpressionFor(KeepexprContext ctx) {
+    KeepsupertypeexprContext supertypeCtx = ctx.keeptypeexpr().keepsupertypeexpr();
+
+    ClassResource typeToKeep = ClassResource.fromQualifiedName(supertypeCtx.classres(0).getText());
+
+    List<KeepExpression> nestedExpressions = ctx.keepattributesexpr().stream()
+        .map(this::visitKeepattributesexpr)
+        .collect(Collectors.toList());
+
+    nestedExpressions.addAll(ctx.keepaggregatesexpr().stream()
+        .map(this::visitKeepaggregatesexpr)
+        .collect(Collectors.toList())
+    );
+
+    nestedExpressions.addAll(ctx.keepexpr().stream()
+        .map(this::visitKeepexpr)
+        .collect(Collectors.toList())
+    );
+
+    KeepSuperTypeExpression.KeepSuperTypeBuilder expressionBuilder = KeepSuperTypeExpression.keep()
+        .supertype(typeToKeep);
+
+    ClassResource targetClass;
+    if (supertypeCtx.classres() != null) {
+      targetClass = ClassResource.fromQualifiedName(supertypeCtx.classres(1).getText());
+    } else {
+      targetClass = typeToKeep;
+    }
+
+    expressionBuilder.as(targetClass);
+    nestedExpressions.forEach(expressionBuilder::keep);
+
+    return expressionBuilder.buildExpression();
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1883fa77954a74ed677d8a5b6cc358ca2721946
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java
@@ -0,0 +1,28 @@
+package org.rosi_project.model_sync.model_join.representation.parser.antlr;
+
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinBaseVisitor;
+import org.rosi_project.model_sync.model_join.representation.parser.antlr.generated.ModelJoinParser.ModeljoinContext;
+import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
+
+/**
+ * The {@code ModelJoinVisitor} creates a {@link ModelJoinExpression} instances from a parsed {@code
+ * ModelJoin} file.
+ *
+ * @author Rico Bergmann
+ */
+public class ModelJoinVisitor extends ModelJoinBaseVisitor<ModelJoinExpression> {
+
+  @Override
+  public ModelJoinExpression visitModeljoin(ModeljoinContext ctx) {
+    JoinStatementVisitor joinVisitor = new JoinStatementVisitor();
+    ModelJoinBuilder modelJoinBuilder = ModelJoinBuilder.createNewModelJoin();
+
+    ctx.join().stream() //
+        .map(joinVisitor::visitJoin) //
+        .forEach(modelJoinBuilder::add);
+
+    return modelJoinBuilder.build();
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6942af6691a6f3699d09ce0f2974327d155b37a7
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/generated/.gitignore
@@ -0,0 +1,8 @@
+/ModelJoin.tokens
+/ModelJoinBaseListener.java
+/ModelJoinBaseVisitor.java
+/ModelJoinLexer.java
+/ModelJoinLexer.tokens
+/ModelJoinListener.java
+/ModelJoinParser.java
+/ModelJoinVisitor.java
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..25f4b9ce3871862e570befcb4f28ad761f467345
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java
@@ -0,0 +1,187 @@
+package org.rosi_project.model_sync.model_join.representation.parser.legacy;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParser;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
+
+/**
+ * The {@code DefaultModelJoinParser} reads a {@code ModelJoin} file from disk and creates matching
+ * representation for it.
+ *
+ * @author Rico Bergmann
+ */
+@Deprecated
+public class DefaultModelJoinParser {
+
+  /**
+   * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}, wrapped into an {@code
+   * Optional}. If parsing fails, an empty {@code Optional} will be returned.
+   */
+  @Nonnull
+  public static Optional<ModelJoinExpression> read(@Nonnull File modelFile) {
+    DefaultModelJoinParser parser = new DefaultModelJoinParser(modelFile, ErrorReportingStrategy.OPTIONAL);
+    return parser.run();
+  }
+
+  /**
+   * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}. If parsing fails, a
+   * {@link ModelJoinParsingException} will be thrown.
+   *
+   * @throws ModelJoinParsingException if the model may not be parsed for some reason.
+   */
+  @Nonnull
+  public static ModelJoinExpression readOrThrow(@Nonnull File modelFile) {
+    DefaultModelJoinParser parser = new DefaultModelJoinParser(modelFile, ErrorReportingStrategy.EXCEPTION);
+    Optional<ModelJoinExpression> result = parser.run();
+
+    if (result.isPresent()) {
+      return result.get();
+    } else {
+      /*
+       * Theoretically, the parser should throw the exception by itself, so this code should never
+       * actually run. It is merely here for safety purposes (unchecked access of Optional.get())
+       */
+      throw new ModelJoinParsingException("Result not present but no exception was thrown either");
+    }
+  }
+
+  /**
+   * Indicates what should happen if parsing fails.
+   */
+  enum ErrorReportingStrategy {
+
+    /**
+     * On failure an empty {@code Optional} should be returned.
+     */
+    OPTIONAL,
+
+    /**
+     * On failure an {@link ModelJoinParsingException} should be thrown.
+     */
+    EXCEPTION
+  }
+
+  private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*$");
+  private static final Pattern IMPORT_STATEMENT = Pattern.compile("^import .*");
+  private static final Pattern TARGET_STATEMENT = Pattern.compile("^target .*");
+  private static final Pattern JOIN_STATEMENT = Pattern
+      .compile("^(((left|right) outer)|theta|natural) join .*");
+
+  @Nonnull
+  private final File modelFile;
+
+  @Nonnull
+  private final ErrorReportingStrategy errorReportingStrategy;
+
+  /**
+   * Full constructor.
+   *
+   * @param modelFile the file to parse
+   * @param errorReportingStrategy what to do in case an error occurs.
+   */
+  private DefaultModelJoinParser(@Nonnull File modelFile,
+      @Nonnull ErrorReportingStrategy errorReportingStrategy) {
+    this.modelFile = modelFile;
+    this.errorReportingStrategy = errorReportingStrategy;
+  }
+
+  /**
+   * Creates the {@code ModelJoin} representation from the {@code modelFile}.
+   * <p>
+   * Depending on the selected {@code errorReportingStrategy} either an Exception will be thrown, or
+   * an empty {@code Optional} will be returned if something goes wrong.
+   */
+  private Optional<ModelJoinExpression> run() {
+    ModelJoinExpression resultingModel;
+    try (FileInputStream modelInputStream = new FileInputStream(modelFile);
+        InputStreamReader modelInputReader = new InputStreamReader(modelInputStream);
+        BufferedReader bufferedModelReader = new BufferedReader(modelInputReader)) {
+
+      ModelJoinBuilder modelJoinBuilder = ModelJoinBuilder.createNewModelJoin();
+      readModelFileAndPopulateBuilder(bufferedModelReader, modelJoinBuilder);
+      resultingModel = modelJoinBuilder.build();
+
+    } catch (ModelJoinParsingException e) {
+      switch (errorReportingStrategy) {
+        case OPTIONAL:
+          return Optional.empty();
+        case EXCEPTION:
+          throw e;
+        default:
+          throw new AssertionError(errorReportingStrategy);
+      }
+    } catch (IOException e) {
+      switch (errorReportingStrategy) {
+        case OPTIONAL:
+          return Optional.empty();
+        case EXCEPTION:
+          throw new ModelJoinParsingException(e);
+        default:
+          throw new AssertionError(errorReportingStrategy);
+      }
+    }
+    return Optional.of(resultingModel);
+  }
+
+  /**
+   * Reads the {@code modelFile} and constructs the corresponding {@code ModelJoin} on the fly.
+   * <p>
+   * On error, an {@code IOException} or an {@link ModelJoinParsingException} will be thrown.
+   *
+   * @throws ModelJoinParsingException if the model file's content are malformed or another
+   *     component failed
+   * @throws IOException if the model file might not be read any further
+   */
+  private void readModelFileAndPopulateBuilder(BufferedReader modelReader,
+      ModelJoinBuilder modelBuilder) throws IOException {
+    String currentLine;
+    while ((currentLine = modelReader.readLine()) != null) {
+      if (lineShouldBeSkipped(currentLine)) {
+        // the line should be skipped. So we do nothing.
+        continue;
+      } else if (lineStartsJoinDeclaration(currentLine)) {
+        // a new join is being declared. Delegate to the join parser to handle the rest.
+        JoinParser joinParser = new JoinParser(currentLine, modelReader);
+        modelBuilder.add(joinParser.run());
+      } else {
+        // we do not know what to do with the current line. Should abort.
+        throw new ModelJoinParsingException("Unexpected line: '" + currentLine + "'");
+      }
+    }
+  }
+
+  /**
+   * Checks, whether the parser should consider the given {@code line} any further, or just ignore
+   * it.
+   * <p>
+   * A line should be skipped, iff.
+   * <ul>
+   * <li>It only consists of whitespace characters</li>
+   * <li>It is an {@code import statement} (as those are not yet considered in the current {@code
+   * ModelJoin} abstraction)</li>
+   * <li>It is a {@code target statement} (as those are not yet considered in the current {@code
+   * ModelJoin} abstraction)</li>
+   * </ul>
+   */
+  private boolean lineShouldBeSkipped(String line) {
+    return EMPTY_LINE.matcher(line).matches() //
+        || IMPORT_STATEMENT.matcher(line).matches() //
+        || TARGET_STATEMENT.matcher(line).matches();
+  }
+
+  /**
+   * Checks, whether the given {@code line} is the beginning of a {@code join statement}.
+   */
+  private boolean lineStartsJoinDeclaration(String line) {
+    return JOIN_STATEMENT.matcher(line).matches();
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee32b6167aa20c1785f1671af0ac52f1a2c46b75
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java
@@ -0,0 +1,254 @@
+package org.rosi_project.model_sync.model_join.representation.parser.legacy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.OuterJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder;
+
+/**
+ * The {@code JoinParser} takes care of reading an individual {@code join statement} and creates the
+ * corresponding {@link JoinExpression}.
+ * <p>
+ * This class is primarily intended to be created and invoked by the {@link DefaultModelJoinParser} only as
+ * will modify the state of that as well.
+ *
+ * @author Rico Bergmann
+ */
+@Deprecated
+class JoinParser {
+
+  private static final String START_OF_JOIN_BLOCK_DELIMITER = "{";
+  private static final String JOIN_TYPE_THETA = "theta";
+  private static final Pattern JOIN_TYPE_DECLARATION = Pattern
+      .compile(".*(?<type>((?<outertype>(right |left )outer)|theta|natural)) join .*");
+  private static final Pattern LEFT_SOURCE_DECLARATION = Pattern
+      .compile(".*join (?<leftsource>[\\w\\\\.]*) .*");
+  private static final Pattern RIGHT_SOURCE_DECLARATION = Pattern
+      .compile(".*with (?<rightsource>[\\w\\\\.]*) .*");
+  private static final Pattern TARGET_DECLARATION = Pattern
+      .compile(".*as (?<target>[\\w\\\\.]*)( .*|\\{)");
+  private static final Pattern THETA_PREDICATE_DECLARATION = Pattern
+      .compile(".*where (?<predicate>[\\w\\\\.]*)\\{");
+
+  @Nonnull
+  private final String startingLine;
+
+  @Nonnull
+  private final BufferedReader modelReader;
+
+  private boolean hasReadStartOfJoinBlockCharacter = false;
+
+  /**
+   * Full constructor.
+   *
+   * @param startingLine the line that was last read by the {@link DefaultModelJoinParser} and should
+   *     therefore become the starting point for {@code this} parser.
+   * @param modelReader the reader that provides access to the following lines in the {@code
+   *     ModelJoin} file currently being read.
+   */
+  JoinParser(@Nonnull String startingLine, @Nonnull BufferedReader modelReader) {
+    this.startingLine = startingLine;
+    this.modelReader = modelReader;
+  }
+
+  /**
+   * Creates the {@link JoinExpression} from the current {@code join statement}.
+   * <p>
+   * If something goes wrong, either an {@code IOException} will be thrown directly or a {@link
+   * ModelJoinParsingException} is used.
+   */
+  @Nonnull
+  public JoinExpression run() throws IOException {
+
+    // first, we will determine the Join type
+
+    Matcher joinTypeDeclarationMatcher = JOIN_TYPE_DECLARATION.matcher(startingLine);
+
+    if (!joinTypeDeclarationMatcher.matches()) {
+      throw new ModelJoinParsingException(
+          "Expected beginning of a Join declaration on '" + startingLine + "'");
+    }
+
+    String joinType = joinTypeDeclarationMatcher.group("type");
+    String outerJoinType = "";
+
+    if (joinType.equals("outer")) {
+      outerJoinType = joinTypeDeclarationMatcher.group("outertype");
+    }
+
+    // second, we will try to extract all necessary info from the first line
+
+    String leftSource = null;
+    String rightSource = null;
+    String target = null;
+    String thetaPredicate = null;
+
+    Matcher leftSourceMatcher = LEFT_SOURCE_DECLARATION.matcher(startingLine);
+    Matcher rightSourceMatcher = RIGHT_SOURCE_DECLARATION.matcher(startingLine);
+    Matcher targetMatcher = TARGET_DECLARATION.matcher(startingLine);
+    Matcher thetaPredicateMatcher = THETA_PREDICATE_DECLARATION.matcher(startingLine);
+
+    final int necessaryComponents;
+    int foundComponents = 0;
+
+    if (leftSourceMatcher.matches()) {
+      leftSource = leftSourceMatcher.group("leftsource");
+      foundComponents++;
+    }
+
+    if (rightSourceMatcher.matches()) {
+      rightSource = rightSourceMatcher.group("rightsource");
+      foundComponents++;
+    }
+
+    if (targetMatcher.matches()) {
+      target = targetMatcher.group("target");
+      foundComponents++;
+    }
+
+    if (joinType.equals(JOIN_TYPE_THETA)) {
+      necessaryComponents = 4;
+      if (thetaPredicateMatcher.matches()) {
+        thetaPredicate = thetaPredicateMatcher.group("predicate");
+        foundComponents++;
+      }
+    } else {
+      necessaryComponents = 3;
+    }
+
+    // in case all information is present on the first line, we are done already
+
+    if (startingLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) {
+      // TODO could the start character be part of an OCL statement?
+      hasReadStartOfJoinBlockCharacter = true;
+    }
+
+    // otherwise, we need to read the following lines as well
+
+    String currentLine = "";
+    while (!hasReadStartOfJoinBlockCharacter //
+        && foundComponents < necessaryComponents //
+        && (currentLine = modelReader.readLine()) != null) {
+
+      // again, we try to extract as much as possible of the info we need
+      // however, we now also have to check, whether we have already specified a component, as well
+
+      leftSourceMatcher = LEFT_SOURCE_DECLARATION.matcher(currentLine);
+      if (leftSource == null && leftSourceMatcher.matches()) {
+        leftSource = leftSourceMatcher.group("leftsource");
+        foundComponents++;
+      }
+
+      rightSourceMatcher = RIGHT_SOURCE_DECLARATION.matcher(currentLine);
+      if (rightSource == null && rightSourceMatcher.matches()) {
+        rightSource = rightSourceMatcher.group("rightsource");
+        foundComponents++;
+      }
+
+      targetMatcher = TARGET_DECLARATION.matcher(currentLine);
+      if (target == null && targetMatcher.matches()) {
+        target = targetMatcher.group("target");
+      }
+
+      if (joinType.equals(JOIN_TYPE_THETA)) {
+        thetaPredicateMatcher = THETA_PREDICATE_DECLARATION.matcher(currentLine);
+        if (thetaPredicate == null && thetaPredicateMatcher.matches()) {
+          thetaPredicate = thetaPredicateMatcher.group("predicate");
+          foundComponents++;
+        }
+      }
+
+      if (currentLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) {
+        hasReadStartOfJoinBlockCharacter = true;
+      }
+    }
+
+    // if we found the beginning of the Join body, but did not read all our information, the
+    // ModelJoin file is flawed
+
+    if (foundComponents < necessaryComponents) {
+      throw new ModelJoinParsingException(
+          "ModelJoin grammar violation: found components " + foundComponents + " but expected "
+              + necessaryComponents);
+    }
+
+    // otherwise we are ready to go. So we skip forward until the Join body is reached.
+
+    while (!hasReadStartOfJoinBlockCharacter && (currentLine = modelReader.readLine()) != null) {
+      if (currentLine.contains(START_OF_JOIN_BLOCK_DELIMITER)) {
+        hasReadStartOfJoinBlockCharacter = true;
+      }
+    }
+
+    // if we read everything, but did not find the Join body, the ModelJoin file is flawed again
+
+    if (!hasReadStartOfJoinBlockCharacter) {
+      throw new ModelJoinParsingException(
+          "Expected start of join block but could not read any further");
+    }
+
+    // if not all components are initialized by now, we are working on a flawed ModelJoin file
+
+    if (currentLine == null || leftSource == null || rightSource == null || target == null) {
+      throw new ModelJoinParsingException(
+          "Not all required information was found but could not read any further");
+    }
+
+    // at this point the declaration of the join statement is valid
+    // therefore we want the keep expressions it contains. This is the KeepParser's job.
+
+    KeepParser keepParser = new KeepParser(currentLine, modelReader);
+    Collection<KeepExpression> keeps = keepParser.run();
+
+    // finally we have to create the correct JoinExpression
+
+    JoinFactory joinFactory = JoinFactory.createNew();
+    switch (joinType) {
+      case "outer":
+        OuterJoinBuilder outerJoinBuilder = joinFactory.outer();
+        if (outerJoinType.equals("left")) {
+          outerJoinBuilder.leftOuter();
+        } else if (outerJoinType.equals("right")) {
+          outerJoinBuilder.rightOuter();
+        } else {
+          throw new ModelJoinParsingException("Unknown direction for outer join: " + outerJoinType);
+        }
+        outerJoinBuilder //
+            .join(ClassResource.fromQualifiedName(leftSource)) //
+            .with(ClassResource.fromQualifiedName(rightSource)) //
+            .as(ClassResource.fromQualifiedName(target));
+        keeps.forEach(outerJoinBuilder::keep);
+        return outerJoinBuilder.done();
+      case "theta":
+        ThetaJoinBuilder thetaJoinBuilder = joinFactory //
+            .theta() //
+            .join(ClassResource.fromQualifiedName(leftSource)) //
+            .with(ClassResource.fromQualifiedName(rightSource)) //
+            .as(ClassResource.fromQualifiedName(target)) //
+            .where(OCLConstraint.of(thetaPredicate));
+        keeps.forEach(thetaJoinBuilder::keep);
+        return thetaJoinBuilder.done();
+      case "natural":
+        NaturalJoinBuilder naturalJoinBuilder = joinFactory //
+            .natural() //
+            .join(ClassResource.fromQualifiedName(leftSource)) //
+            .with(ClassResource.fromQualifiedName(rightSource)) //
+            .as(ClassResource.fromQualifiedName(target));
+        keeps.forEach(naturalJoinBuilder::keep);
+        return naturalJoinBuilder.done();
+      default:
+        throw new ModelJoinParsingException("Unknown join type: " + joinType);
+    }
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac82f383b79ed8a38b3a7590daf4bbe5843d4d99
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java
@@ -0,0 +1,98 @@
+package org.rosi_project.model_sync.model_join.representation.parser.legacy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression;
+import org.rosi_project.model_sync.model_join.representation.parser.ModelJoinParsingException;
+
+/**
+ * The {@code KeepParser} takes care of reading the {@code keep statements} and creating the
+ * corresponding {@link KeepExpression}s for the body of an {@code join statement}.
+ * <p>
+ * This class is primarily intended to be created and invoked by the {@link JoinParser} only as will
+ * modify the state of that as well.
+ *
+ * @author Rico Bergmann
+ */
+@Deprecated
+class KeepParser {
+
+  private static final String END_OF_JOIN_BLOCK_DELIMITER = "}";
+  private static final Pattern KEEP_ATTRIBUTE_DECLARATION = Pattern
+      .compile(".*keep attributes\\W*(?<attribute>[\\w\\\\.]*)$");
+
+  @Nonnull
+  private final String startingLine;
+
+  @Nonnull
+  private final BufferedReader modelReader;
+
+  private boolean hasReadEndOfJoinBlockDelimiter = false;
+
+  /**
+   * Full constructor.
+   * @param startingLine the line that was last read by the {@link JoinParser} and should
+   *     therefore become the starting point for {@code this} parser.
+   * @param modelReader the reader that provides access to the following lines in the {@code
+   *     ModelJoin} file currently being read.
+   */
+  KeepParser(@Nonnull String startingLine, @Nonnull BufferedReader modelReader) {
+    this.startingLine = startingLine;
+    this.modelReader = modelReader;
+  }
+
+  /**
+   * Creates the {@link KeepExpression}s for the current {@code join statement}.
+   * <p>
+   * If something goes wrong, either an {@code IOException} will be thrown directly or a {@link
+   * ModelJoinParsingException} is used.
+   */
+  @Nonnull
+  public Collection<KeepExpression> run() throws IOException {
+    List<KeepExpression> detectedKeeps = new LinkedList<>();
+    Matcher keepMatcher = KEEP_ATTRIBUTE_DECLARATION.matcher(startingLine);
+
+    parseLine(startingLine, keepMatcher).ifPresent(detectedKeeps::add);
+
+    String currentLine;
+    while (!hasReadEndOfJoinBlockDelimiter && (currentLine = modelReader.readLine()) != null) {
+      keepMatcher = KEEP_ATTRIBUTE_DECLARATION.matcher(currentLine);
+      parseLine(currentLine, keepMatcher).ifPresent(detectedKeeps::add);
+    }
+
+    return detectedKeeps;
+  }
+
+  /**
+   * Tries to create a {@link KeepExpression} for a single {@code keep statement} written on the
+   * {@code current line}.
+   * <p>
+   * If there is no such statement, an empty {@code Optional} will be returned.
+   * <p>
+   * If the line indicates the end of the keep-block, the state of {@code this} parser will be
+   * updated accordingly.
+   */
+  @Nonnull
+  private Optional<KeepExpression> parseLine(String currentLine, Matcher keepMatcher) {
+    Optional<KeepExpression> result = Optional.empty();
+    if (keepMatcher.matches()) {
+      AttributePath attribute = AttributePath.fromQualifiedPath(keepMatcher.group("attribute"));
+      result = Optional.of(KeepAttributesExpression.keepAttributes(attribute));
+    }
+
+    if (currentLine.contains(END_OF_JOIN_BLOCK_DELIMITER)) {
+      hasReadEndOfJoinBlockDelimiter = true;
+    }
+    return result;
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java
new file mode 100644
index 0000000000000000000000000000000000000000..d72ecca9b380b86828c5134808cef2e48bc46cd8
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Assert.java
@@ -0,0 +1,36 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Utility class to provide a number of useful assertion mechanisms.
+ * <p>
+ * On failure, an {@code AssertionError} with a dedicated message will be thrown.
+ *
+ * @author Rico Bergmann
+ */
+public class Assert {
+
+  /**
+   * Ensures that an object is not {@code null}.
+   */
+  public static void notNull(Object obj, @Nonnull String failureMessage) {
+    if (obj == null) {
+      throw new AssertionError(failureMessage);
+    }
+  }
+
+  /**
+   * Ensures that none of the arguments is {@code null}.
+   */
+  public static void noNullArguments(@Nonnull String failureMessage, Object... arguments) {
+    if (arguments == null) {
+      throw new AssertionError("Vararg arguments may not be null");
+    }
+    for (Object arg : arguments) {
+      if (arg == null) {
+        throw new AssertionError(failureMessage);
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java
new file mode 100644
index 0000000000000000000000000000000000000000..853ee96122a43dfe41d4269dcaf6feb592b284d9
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Functional.java
@@ -0,0 +1,235 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.swing.text.html.Option;
+
+/**
+ * Contains a number of useful classes to mimic behaviour from functional programming languages in
+ * Java.
+ *
+ * @author Rico Bergmann
+ */
+public class Functional {
+
+  /**
+   * Emulates a dispatch on the concrete type of an object. Depending on the type, different control
+   * paths will be executed.
+   *
+   * @param something the object to match on
+   */
+  @Nonnull
+  public static <R> FunctionalMatch<R> match(@Nonnull Object something) {
+    return new FunctionalMatch<>(something);
+  }
+
+  /**
+   *
+   * @author Rico Bergmann
+   *
+   * @param <R>
+   */
+  public static class FunctionalMatch<R> extends Functional {
+
+    @Nonnull
+    private final Object matchInstance;
+
+    @Nonnull
+    private final List<Pair<Class<?>, CaseResult<R>>> functions;
+
+    @Nullable
+    private CaseResult<R> defaultAction;
+
+    /**
+     *
+     * @param matchInstance
+     */
+    private FunctionalMatch(@Nonnull Object matchInstance) {
+      Assert.notNull(matchInstance, "Object to match on may not be null");
+      this.matchInstance = matchInstance;
+      this.functions = new ArrayList<>();
+    }
+
+    /**
+     * @param clazz
+     * @param action
+     * @return
+     */
+    @SuppressWarnings("unchecked")
+    public <T> FunctionalMatch<R> caseOf(@Nonnull Class<T> clazz, @Nonnull Function<T, R> action) {
+      Assert.notNull(clazz, "Class to match on may not be null");
+      Assert.notNull(action, "Action to perform may not be null");
+      functions.add(
+          Pair.of(clazz, new RunnableCaseResult<>((Function<Object, R>) action, matchInstance)));
+      return this;
+    }
+
+    /**
+     * @param clazz
+     * @param result
+     * @return
+     */
+    public <T> FunctionalMatch<R> caseOf(@Nonnull Class<T> clazz, @Nonnull R result) {
+      Assert.notNull(clazz, "Class to match on may not be null");
+      Assert.notNull(result, "Match result may not be null");
+      functions.add(Pair.of(clazz, new StaticCaseResult<>(result)));
+      return this;
+    }
+
+    /**
+     * @param action
+     * @return
+     */
+    public FunctionalMatch<R> defaultCase(@Nonnull Function<? super Object, R> action) {
+      Assert.notNull(action, "Default action may not be null");
+      this.defaultAction = new RunnableCaseResult<>(action, matchInstance);
+      return this;
+    }
+
+    /**
+     * @param result
+     * @return
+     */
+    public FunctionalMatch<R> defaultCase(R result) {
+      Assert.notNull(result, "Match result may not be null");
+      this.defaultAction = new StaticCaseResult<>(result);
+      return this;
+    }
+
+    /**
+     * Performs the match.
+     * <p>
+     * This will check for the first case to which the match object is assignable and execute the
+     * associated action.
+     *
+     * @return the result of the action or {@code null} if no matching case was specified.
+     */
+    public R run() {
+      try {
+        R res = runOrThrow();
+        return res;
+      } catch (MatchException e) {
+        return null;
+      }
+    }
+
+    public Optional<R> tryRun() {
+      try {
+        R res = runOrThrow();
+        return Optional.of(res);
+      } catch (MatchException e) {
+        return Optional.empty();
+      }
+    }
+
+    /**
+     * Attempts to perform the match.
+     * <p>
+     * This will check for the first case to which the match object is assignable and execute the
+     * associated action. If no matching action was specified, an exception will be raised.
+     *
+     * @return the result of the action or {@code null} if no matching case was specified.
+     * @throws MatchException if no matching action was given.
+     */
+    public R runOrThrow() {
+      for (Pair<Class<?>, CaseResult<R>> caseEntry : functions) {
+        Class<?> entryClass = caseEntry.getFirst();
+        if (entryClass.isAssignableFrom(matchInstance.getClass())) {
+          return caseEntry.getSecond().calculate();
+        }
+      }
+      if (defaultAction != null) {
+        return defaultAction.calculate();
+      }
+      throw new MatchException(matchInstance.getClass(), "No case specified");
+    }
+
+    private static abstract class CaseResult<R> {
+
+      abstract R calculate();
+
+    }
+
+    private static class RunnableCaseResult<R> extends CaseResult<R> {
+
+      private final Function<Object, R> action;
+      private final Object param;
+
+      /**
+       * @param action
+       * @param param
+       */
+      RunnableCaseResult(@Nonnull Function<Object, R> action, @Nonnull Object param) {
+        Assert.notNull(action, "Action may not be null");
+        Assert.notNull(param, "Param may not be null");
+        this.action = action;
+        this.param = param;
+      }
+
+      /*
+       * (non-Javadoc)
+       *
+       * @see de.naju.adebar.util.Functional.FunctionalMatch.CaseResult#calculate()
+       */
+      @Override
+      R calculate() {
+        return action.apply(param);
+      }
+
+    }
+
+    private static class StaticCaseResult<R> extends CaseResult<R> {
+
+      private final R result;
+
+      /**
+       * @param result
+       */
+      public StaticCaseResult(R result) {
+        this.result = result;
+      }
+
+      /*
+       * (non-Javadoc)
+       *
+       * @see de.naju.adebar.util.Functional.FunctionalMatch.CaseResult#calculate()
+       */
+      @Override
+      R calculate() {
+        return result;
+      }
+
+    }
+
+  }
+
+  public static class MatchException extends RuntimeException {
+
+    private static final long serialVersionUID = -3903882205980399140L;
+
+    private final Class<?> actualClass;
+
+    MatchException(Class<?> actualClass) {
+      this.actualClass = actualClass;
+    }
+
+    MatchException(Class<?> actualClass, String message) {
+      super(message);
+      this.actualClass = actualClass;
+    }
+
+    /**
+     * @return the actualClass
+     */
+    public Class<?> getActualClass() {
+      return actualClass;
+    }
+
+  }
+
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd873738be32d3169868662f1f048e01480db08e
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/JoinFactory.java
@@ -0,0 +1,302 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.OCLConstraint;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression.JoinDirection;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression;
+
+/**
+ * The {@code JoinFactory} should be used as the main facility to create new {@link JoinExpression}
+ * instances. It supports instantiation for all subclasses and provides a fluid and natural
+ * interface to set them up.
+ *
+ * @author Rico Bergmann
+ */
+public class JoinFactory {
+
+  /**
+   * Starts the construction process for a new join.
+   */
+  @Nonnull
+  public static JoinFactory createNew() {
+    return new JoinFactory();
+  }
+
+  private JoinFactory() {
+    // pass
+  }
+
+  /**
+   * Constructs a new {@link OuterJoinExpression outer join}
+   */
+  @Nonnull
+  public OuterJoinBuilder outer() {
+    return new OuterJoinBuilder();
+  }
+
+  /**
+   * Constructs a new {@link NaturalJoinExpression natural join}.
+   */
+  @Nonnull
+  public NaturalJoinBuilder natural() {
+    return new NaturalJoinBuilder();
+  }
+
+  /**
+   * Constructs a new {@link ThetaJoinExpression theta join}.
+   */
+  @Nonnull
+  public ThetaJoinBuilder theta() {
+    return new ThetaJoinBuilder();
+  }
+
+  /**
+   * This is the base for all actual join builders and contains the fields that have to be present
+   * in each join. Consult the documentation of the {@link JoinExpression} class for details on
+   * those fields.
+   * <p>
+   * Despite all builders working very similarly, they may not be refactored to have more logic
+   * provided through this class due to limitations of the Java language.
+   *
+   * @see JoinExpression
+   */
+  public static abstract class AbstractJoinBuilder {
+    protected ClassResource left;
+    protected ClassResource right;
+    protected ClassResource target;
+    protected List<KeepExpression> keeps = new ArrayList<>();
+  }
+
+  /**
+   * The {@code OuterJoinBuilder} takes care of setting up new {@link OuterJoinExpression}
+   * instances.
+   *
+   * @see OuterJoinExpression
+   */
+  public static class OuterJoinBuilder extends AbstractJoinBuilder {
+
+    private OuterJoinExpression.JoinDirection direction;
+
+    /**
+     * Default constructor.
+     * <p>
+     * {@code private} to prevent direct instantiation.
+     */
+    private OuterJoinBuilder() {
+      // pass
+    }
+
+    /**
+     * Configures the join to use the right source model as the primary one.
+     */
+    @Nonnull
+    public OuterJoinBuilder rightOuter() {
+      direction = JoinDirection.RIGHT;
+      return this;
+    }
+
+    /**
+     * Configures the join to use the left source model as the primary one.
+     */
+    @Nonnull
+    public OuterJoinBuilder leftOuter() {
+      direction = JoinDirection.LEFT;
+      return this;
+    }
+
+    /**
+     * Specifies the left source.
+     */
+    @Nonnull
+    public OuterJoinBuilder join(@Nonnull ClassResource left) {
+      this.left = left;
+      return this;
+    }
+
+    /**
+     * Specifies the right source.
+     */
+    @Nonnull
+    public OuterJoinBuilder with(@Nonnull ClassResource right) {
+      this.right = right;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the resulting class.
+     */
+    @Nonnull
+    public OuterJoinBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Specifies a field that should be contained in the resulting view.
+     */
+    @Nonnull
+    public OuterJoinBuilder keep(@Nonnull KeepExpression keep) {
+      this.keeps.add(keep);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process and provides the resulting {@code join}.
+     */
+    @Nonnull
+    public OuterJoinExpression done() {
+      return new OuterJoinExpression(left, right, target, direction, keeps);
+    }
+  }
+
+  /**
+   * The {@code NaturalJoinBuilder} takes care of setting up new {@link NaturalJoinExpression}
+   * instances.
+   *
+   * @see NaturalJoinExpression
+   */
+  public static class NaturalJoinBuilder extends AbstractJoinBuilder {
+
+    /**
+     * Default constructor.
+     * <p>
+     * {@code private} to prevent direct instantiation.
+     */
+    private NaturalJoinBuilder() {
+      // pass
+    }
+
+    /**
+     * Specifies the left source.
+     */
+    @Nonnull
+    public NaturalJoinBuilder join(@Nonnull ClassResource left) {
+      this.left = left;
+      return this;
+    }
+
+    /**
+     * Specifies the right source.
+     */
+    @Nonnull
+    public NaturalJoinBuilder with(@Nonnull ClassResource right) {
+      this.right = right;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the resulting class.
+     */
+    @Nonnull
+    public NaturalJoinBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Specifies a field that should be contained in the resulting view.
+     */
+    @Nonnull
+    public NaturalJoinBuilder keep(@Nonnull KeepExpression keep) {
+      this.keeps.add(keep);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process and provides the resulting {@code join}.
+     */
+    @Nonnull
+    public NaturalJoinExpression done() {
+      return new NaturalJoinExpression(left, right, target, keeps);
+    }
+  }
+
+  /**
+   * The {@code ThetaJoinBuilder} takes care of setting up new {@link ThetaJoinExpression}
+   * instances.
+   *
+   * @see ThetaJoinExpression
+   */
+  public static class ThetaJoinBuilder extends AbstractJoinBuilder {
+
+    private OCLConstraint condition;
+
+    /**
+     * Default constructor.
+     * <p>
+     * {@code private} to prevent direct instantiation.
+     */
+    private ThetaJoinBuilder() {
+      // pass
+    }
+
+    /**
+     * Specifies the left source.
+     */
+    @Nonnull
+    public ThetaJoinBuilder join(@Nonnull ClassResource left) {
+      this.left = left;
+      return this;
+    }
+
+    /**
+     * Specifies the right source.
+     */
+    @Nonnull
+    public ThetaJoinBuilder with(@Nonnull ClassResource right) {
+      this.right = right;
+      return this;
+    }
+
+    /**
+     * Specifies the name of the resulting class.
+     */
+    @Nonnull
+    public ThetaJoinBuilder as(@Nonnull ClassResource target) {
+      this.target = target;
+      return this;
+    }
+
+    /**
+     * Specifies the join condition.
+     */
+    @Nonnull
+    public ThetaJoinBuilder where(@Nonnull OCLConstraint condition) {
+      this.condition = condition;
+      return this;
+    }
+
+    /**
+     * Specifies the join condition.
+     */
+    @Nonnull
+    public ThetaJoinBuilder where(@Nonnull String oclCondition) {
+      return where(OCLConstraint.of(oclCondition));
+    }
+
+    /**
+     * Specifies a field that should be contained in the resulting view.
+     */
+    @Nonnull
+    public ThetaJoinBuilder keep(@Nonnull KeepExpression keep) {
+      this.keeps.add(keep);
+      return this;
+    }
+
+    /**
+     * Finishes the construction process and provides the resulting {@code join}.
+     */
+    @Nonnull
+    public ThetaJoinExpression done() {
+      return new ThetaJoinExpression(left, right, target, condition, keeps);
+    }
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..c92b525cc3cab941a9524c8b396b0de39958d25c
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/ModelJoinBuilder.java
@@ -0,0 +1,79 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+import java.util.LinkedList;
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression.KeepReferenceBuilder;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression.KeepSuperTypeBuilder;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.NaturalJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.ThetaJoinBuilder;
+
+/**
+ * The {@code ModelJoinBuilder} provides a nice and fluent way of creating new {@code ModelJoin}
+ * instances.
+ *
+ * @author Rico Bergmann
+ */
+public class ModelJoinBuilder {
+
+  public static ThetaJoinBuilder thetaJoin() {
+    return JoinFactory.createNew().theta();
+  }
+  
+  public static NaturalJoinBuilder naturalJoin() {
+	  return JoinFactory.createNew().natural();
+  }
+
+  public static KeepAttributesExpression attributes(AttributePath firstAttribute, AttributePath... moreAttributes) {
+    return KeepAttributesExpression.keepAttributes(firstAttribute, moreAttributes);
+  }
+
+  public static KeepReferenceBuilder outgoing(AttributePath attr) {
+    return KeepReferenceExpression.keep().outgoing(attr);
+  }
+
+  public static KeepSuperTypeBuilder supertype(ClassResource supertype) {
+    return KeepSuperTypeExpression.keep().supertype(supertype);
+  }
+
+  /**
+   * Starts the construction process for a new {@link ModelJoinExpression}.
+   */
+  @Nonnull
+  public static ModelJoinBuilder createNewModelJoin() {
+    return new ModelJoinBuilder();
+  }
+
+  private List<JoinExpression> joinBuffer = new LinkedList<>();
+
+  /**
+   * Full/default constructor.
+   */
+  private ModelJoinBuilder() {
+    // pass
+  }
+
+  /**
+   * Extends the current {@code ModelJoin} by another join.
+   */
+  @Nonnull
+  public ModelJoinBuilder add(@Nonnull JoinExpression join) {
+    joinBuffer.add(join);
+    return this;
+  }
+
+  /**
+   * Finishes the construction process and provides the resulting {@code ModelJoin}.
+   */
+  @Nonnull
+  public ModelJoinExpression build() {
+    return new ModelJoinExpression(joinBuffer);
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f4ddf4e0686a58f691c788bf2c71a5bcabde1b4
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Nothing.java
@@ -0,0 +1,11 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+public final class Nothing {
+
+  public static Nothing __() {
+    return new Nothing();
+  }
+
+  private Nothing() {};
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java
new file mode 100644
index 0000000000000000000000000000000000000000..8cfb469171b91948577ec7a0016ecba646cd7029
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/util/Pair.java
@@ -0,0 +1,48 @@
+package org.rosi_project.model_sync.model_join.representation.util;
+
+import java.util.Objects;
+
+public class Pair<F, S> {
+
+  public static <F, S> Pair<F, S> of(F first, S second) {
+    return new Pair<F, S>(first, second);
+  }
+
+  private final F first;
+  private final S second;
+
+  public Pair(F first, S second) {
+    this.first = first;
+    this.second = second;
+  }
+
+  public F getFirst() {
+    return first;
+  }
+
+  public S getSecond() {
+    return second;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof Pair)) {
+      return false;
+    }
+    Pair<?, ?> pair = (Pair<?, ?>) o;
+    return Objects.equals(first, pair.first) &&
+        Objects.equals(second, pair.second);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(first, second);
+  }
+
+  public String toString() {
+    return "(" + first + ", " + second + ")";
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9cc804f59a7bbf116cfc0916c61381b322e9401
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriter.java
@@ -0,0 +1,51 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+/**
+ * The {@code FileBasedModelJoinWriter} outputs a {@link ModelJoinExpression model join description}
+ * onto the File System.
+ *
+ * @author Rico Bergmann
+ */
+public class FileBasedModelJoinWriter implements ModelJoinWritingService<Boolean> {
+
+  private final File outputFile;
+  private final StringBasedModelJoinWriter toStringWriter;
+
+  /**
+   * Full constructor.
+   *
+   * @param outputFile the file to write to
+   */
+  public FileBasedModelJoinWriter(@Nonnull File outputFile) {
+    this.outputFile = outputFile;
+    this.toStringWriter = StringBasedModelJoinWriter.withNewlines();
+  }
+
+  /**
+   * Writes the given description to the {@code outputFile}.
+   *
+   * @return whether the file was successfully written
+   */
+  @Override
+  public Boolean write(@Nonnull ModelJoinExpression modelJoin) {
+    try (BufferedWriter writer = Files.newBufferedWriter(
+        outputFile.toPath(),
+        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+
+      String modelJoinTextRepresentation = toStringWriter.write(modelJoin);
+      writer.write(modelJoinTextRepresentation);
+
+      return true;
+    } catch (IOException ioe) {
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..96d3434153904743c2f634cb890be8ba823e32a8
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWriter.java
@@ -0,0 +1,42 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import java.io.File;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+/**
+ * The {@code ModelJoinWriter} acts as an entry point to write and or transform {@link
+ * ModelJoinExpression model join descriptions} to various representations.
+ *
+ * @author Rico Bergmann
+ */
+public class ModelJoinWriter {
+
+  private static final StringBasedModelJoinWriter stringWriter = StringBasedModelJoinWriter
+      .createDefault();
+
+  /**
+   * Converts a whole model join description to a single {@code String}.
+   * <p>
+   * This {@code String} is primarily intended to be human-readable and does not focus in being easy
+   * to reuse for further purposes.
+   */
+  public static String writeToString(@Nonnull ModelJoinExpression modelJoin) {
+    return stringWriter.write(modelJoin);
+  }
+
+  /**
+   * Outputs a whole model join description to a single file.
+   * <p>
+   * The resulting file is intended to be both human-readable as well as to be parsed by further
+   * applications.
+   *
+   * @return whether the expression was written successfully.
+   */
+  public static boolean writeToFile(
+      @Nonnull File outputFile,
+      @Nonnull ModelJoinExpression modelJoin) {
+    return new FileBasedModelJoinWriter(outputFile).write(modelJoin);
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f89772ac9e62bef6ad2952892b60862cfcd6510
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/ModelJoinWritingService.java
@@ -0,0 +1,20 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+
+/**
+ * A {@code ModelJoinWritingService} transforms a {@link ModelJoinExpression model join description}
+ * into some different representation.
+ *
+ * @author Rico Bergmann
+ */
+public interface ModelJoinWritingService<T> {
+
+  /**
+   * Performs the transformation. The result of this method depends on the actual details of the
+   * writing service and should be documented there.
+   */
+  T write(@Nonnull ModelJoinExpression modelJoin);
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..8281979371398a408c082622e8d6d10fa37de98d
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriter.java
@@ -0,0 +1,160 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import java.util.StringJoiner;
+import javax.annotation.Nonnull;
+import org.rosi_project.model_sync.model_join.representation.grammar.CompoundKeepExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.JoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAggregateExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepAttributesExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepCalculatedExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepReferenceExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSubTypeExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.KeepSuperTypeExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.NaturalJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.OuterJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.grammar.ThetaJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.util.Functional;
+import org.rosi_project.model_sync.model_join.representation.util.Nothing;
+
+/**
+ * The {@code StringBasedModelJoinWriter} transforms a {@link ModelJoinExpression model join description}
+ * to a single {@code String}.
+ *
+ * @author Rico Bergmann
+ */
+class StringBasedModelJoinWriter implements ModelJoinWritingService<String> {
+
+  private static final String NEWLINE_DELIM = "\n";
+  private static final String EMPTY_DELIM = " ";
+  private static final String DEFAULT_INDENTATION = "  ";
+
+  static StringBasedModelJoinWriter createDefault() {
+    return new StringBasedModelJoinWriter(false);
+  }
+
+  static StringBasedModelJoinWriter withNewlines() {
+    return new StringBasedModelJoinWriter(true);
+  }
+
+  private final String delimiter;
+  private final boolean useNewlineDelimiters;
+
+  private String currentIndentation = "";
+
+  private StringBasedModelJoinWriter(boolean useNewlinesDelimiters) {
+    if (useNewlinesDelimiters) {
+      this.delimiter = NEWLINE_DELIM;
+    } else {
+      this.delimiter = EMPTY_DELIM;
+    }
+    this.useNewlineDelimiters = useNewlinesDelimiters;
+  }
+
+  @Override
+  public String write(@Nonnull ModelJoinExpression modelJoin) {
+    StringJoiner joiner = new StringJoiner(delimiter + delimiter);
+
+    modelJoin.getJoins().forEach(
+        joinStatement -> joiner.merge(writeJoin(joinStatement))
+    );
+
+    return joiner.toString();
+  }
+
+  private StringJoiner writeJoin(@Nonnull JoinExpression join) {
+    StringJoiner joiner = new StringJoiner(" ");
+
+    String joinHeader = Functional.<String>match(join)
+        .caseOf(ThetaJoinExpression.class, "theta join")
+        .caseOf(NaturalJoinExpression.class, "natural join")
+        .caseOf(OuterJoinExpression.class, outerJoin -> {
+          switch (outerJoin.getDirection()) {
+            case LEFT:
+              return "left outer join";
+            case RIGHT:
+              return "right outer join";
+            default:
+              throw new AssertionError(outerJoin.getDirection());
+          }
+        })
+        .runOrThrow();
+    joiner.add(String.format(currentIndentation + "%s %s with %s as %s", joinHeader, join.getLeft(), join.getRight(), join.getTarget()));
+    Functional.<Nothing>match(join)
+        .caseOf(ThetaJoinExpression.class, thetaJoin -> {
+          joiner.add("where").add(thetaJoin.getCondition().toString());
+          return Nothing.__();
+        })
+        .defaultCase(Nothing.__())
+        .runOrThrow();
+
+    joiner.add("{\n");
+    increaseIndentationIfNecessary();
+    join.getKeeps().forEach(keep -> joiner.add(currentIndentation + writeKeep(keep) + delimiter));
+    decreaseIndentationIfNecessary();
+    joiner.add(currentIndentation + "}");
+
+    return joiner;
+  }
+
+  private String writeKeep(KeepExpression keep) {
+
+    return Functional.<String>match(keep)
+        .caseOf(KeepAttributesExpression.class, keepAttributes -> {
+          StringJoiner attributesJoiner = new StringJoiner(", ");
+          keepAttributes.getAttributes().forEach(attr -> attributesJoiner.add(attr.toString()));
+          return "keep attributes " + attributesJoiner;
+        })
+        .caseOf(KeepCalculatedExpression.class, keepCalculated ->
+            "keep calculated attribute "
+                + keepCalculated.getCalculationRule()
+                + " as " + keepCalculated.getTarget())
+        .caseOf(KeepAggregateExpression.class, keepAggregate ->
+            String.format("keep aggregate %s(%s) over %s as %s",
+                keepAggregate.getAggregation().name().toLowerCase(),
+                keepAggregate.getAggregatedAttribute(),
+                keepAggregate.getSource(), keepAggregate.getTarget())
+            )
+        .caseOf(KeepReferenceExpression.class, keepRef -> {
+          String header = String.format("keep %s %s as type %s {" + delimiter, keepRef.getReferenceDirection().name().toLowerCase(), keepRef.getAttribute(), keepRef.getTarget());
+          return writeCompound(keepRef, header);
+        })
+        .caseOf(KeepSubTypeExpression.class, keepSubtype -> {
+          String header = String.format("keep subtype %s as type %s {" + delimiter, keepSubtype.getType(), keepSubtype.getTarget());
+          return writeCompound(keepSubtype, header);
+        })
+        .caseOf(KeepSuperTypeExpression.class, keepSupertype -> {
+          String header = String.format("keep supertype %s as type %s {" + delimiter, keepSupertype.getType(), keepSupertype.getTarget());
+          return writeCompound(keepSupertype, header);
+        })
+        .runOrThrow();
+
+  }
+
+  private String writeCompound(CompoundKeepExpression compoundKeep, String header) {
+    StringJoiner refJoiner = new StringJoiner(" ");
+
+    refJoiner.add(header);
+    increaseIndentationIfNecessary();
+    compoundKeep.getKeeps().forEach(keep -> refJoiner.add(currentIndentation + writeKeep(keep) + delimiter));
+    decreaseIndentationIfNecessary();
+    refJoiner.add(currentIndentation + "}");
+
+    return refJoiner.toString();
+  }
+
+  private void increaseIndentationIfNecessary() {
+    if (useNewlineDelimiters) {
+      currentIndentation += DEFAULT_INDENTATION;
+    }
+  }
+
+  private  void decreaseIndentationIfNecessary() {
+    if (useNewlineDelimiters) {
+      int deletionIdx = currentIndentation.lastIndexOf(DEFAULT_INDENTATION);
+      currentIndentation = currentIndentation.substring(0, deletionIdx);
+    }
+  }
+
+}
diff --git a/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java b/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java
new file mode 100644
index 0000000000000000000000000000000000000000..44fa819a32695600f414d3adb075e0d8c4411cb6
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/modelrepresentation/ModelJoinCreation.java
@@ -0,0 +1,131 @@
+package org.rosi_project.model_sync.modelrepresentation;
+
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.naturalJoin;
+
+import java.io.File;
+
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
+import org.rosi_project.model_sync.model_join.representation.writer.*;
+
+public class ModelJoinCreation {
+
+	public static void main(String[] args) {
+		/*ClassResource person = ClassResource.fromQualifiedName("contact.Person");
+		ClassResource employee = ClassResource.fromQualifiedName("company.Employee");
+		AttributePath dateOfBirth = AttributePath.from(person, "dateOfBirth");
+
+		ModelJoinExpression mj = ModelJoinBuilder.createNewModelJoin()
+		  .add(JoinFactory.createNew()
+		    .natural()
+		    .join(person)
+		    .with(employee)
+		    .as(ClassResource.fromQualifiedName("unified.Person"))
+		    .keep(KeepAttributesExpression
+		      .keepAttributes(dateOfBirth))
+		    .done()
+		  ).build();
+		System.out.println(mj);
+		for (JoinExpression j: mj.getJoins()) {
+			System.out.println(j);
+			for (KeepExpression k: j.getKeeps()) {
+				System.out.println(k);
+			}
+		}*/
+		
+		ClassResource library = ClassResource.from("lib", "Library");
+	    ClassResource employee = ClassResource.from("lib", "Employee");
+	    ClassResource person = ClassResource.from("lib", "Person");
+	    
+	    //ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie");
+	    //ClassResource jointargetVote = ClassResource.from("jointarget", "Vote");
+	    //ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem");
+
+	    AttributePath libraryName = AttributePath.from(library, "name");
+	    AttributePath libraryEmployees = AttributePath.from(library, "employees");
+	    AttributePath employeeSalary = AttributePath.from(employee, "salary");
+	    AttributePath employeeManager = AttributePath.from(employee, "manager");
+	    AttributePath personName = AttributePath.from(person, "name");
+	    
+	    ModelJoinExpression mjManager = ModelJoinBuilder.createNewModelJoin()
+		        .add(naturalJoin()
+		            .join(employee)
+		            .with(employee)
+		            .as(employee)
+		            .keep(attributes(employeeSalary))
+		            .keep(outgoing(employeeManager)
+		                .as(employee)
+		                .buildExpression()
+		            )
+		            .keep(supertype(person)
+			                .as(person)
+			                .keep(attributes(personName))
+			                .buildExpression()
+			        )
+		            .done())
+		        .build();
+	    
+		ModelJoinExpression mjComplete = ModelJoinBuilder.createNewModelJoin()
+		        .add(naturalJoin()
+		            .join(library)
+		            .with(library)
+		            .as(library)
+		            .keep(attributes(libraryName))
+		            .keep(outgoing(libraryEmployees)
+		                .as(employee)
+		                .keep(attributes(employeeSalary))
+		                .keep(outgoing(employeeManager)
+		                		.as(employee)
+		                		.buildExpression()
+		                )
+		                .keep(supertype(person)
+				                .as(person)
+				                .keep(attributes(personName))
+				                .buildExpression()
+				        )
+		                .buildExpression()
+		            )
+		            .done())
+		        .build();
+		
+		ModelJoinExpression mjSimple = ModelJoinBuilder.createNewModelJoin()
+		        .add(naturalJoin()
+		            .join(library)
+		            .with(library)
+		            .as(library)
+		            .keep(attributes(libraryName))
+		            .keep(outgoing(libraryEmployees)
+		                .as(employee)
+		                .keep(attributes(employeeSalary))
+		                .keep(supertype(person)
+				                .as(person)
+				                .keep(attributes(personName))
+				                .buildExpression()
+				        )
+		                .buildExpression()
+		            )
+		            .done())
+		        .build();
+		
+		File fileComplete = new File("libraryComplete.modeljoin");
+		File fileSimple = new File("librarySimple.modeljoin");
+		File fileManager = new File("manager.modeljoin");
+	    //registerCreatedFile(outputFile);
+
+	    FileBasedModelJoinWriter writerComplete = new FileBasedModelJoinWriter(fileComplete);
+	    writerComplete.write(mjComplete);
+	    
+	    FileBasedModelJoinWriter writerSimple = new FileBasedModelJoinWriter(fileSimple);
+	    writerSimple.write(mjSimple);
+	    
+	    FileBasedModelJoinWriter writerManager = new FileBasedModelJoinWriter(fileManager);
+	    writerManager.write(mjManager);
+	}
+
+}
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..115556369bdefdd7850579d6c81c04ceb348390f
--- /dev/null
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/FileBasedModelJoinWriterAcceptanceTests.java
@@ -0,0 +1,64 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2;
+
+class FileBasedModelJoinWriterAcceptanceTests {
+
+  private static final Logger log = Logger.getLogger(FileBasedModelJoinWriterAcceptanceTests.class.getSimpleName());
+
+  private List<File> createdFiles = new ArrayList<>();
+
+  @AfterEach
+  void tearDown() {
+    createdFiles.forEach(file -> {
+      boolean result = file.delete();
+      log.info(String.format("Deleting file %s, successfull=%s", file, result));
+    });
+    createdFiles.clear();
+  }
+
+  /**
+   * Depends on {@link StringBasedModelJoinWriterAcceptanceTests#writeProducesListing2FromTechnicalReport()}
+   */
+  @Test
+  void writeProducesListing2FromTechnicalReport() {
+    File outputFile = new File("listing2.modeljoin");
+    registerCreatedFile(outputFile);
+
+    FileBasedModelJoinWriter writer = new FileBasedModelJoinWriter(outputFile);
+    writer.write(Listing2.asModelJoin);
+
+    logFileContent(outputFile);
+
+    String expectedContent = StringBasedModelJoinWriter.withNewlines().write(Listing2.asModelJoin);
+    assertThat(outputFile).exists();
+    assertThat(outputFile).hasContent(expectedContent);
+  }
+
+  private void registerCreatedFile(File newFile) {
+    createdFiles.add(newFile);
+    log.info(String.format("Created new file %s", newFile));
+  }
+
+  private void logFileContent(File file) {
+    try {
+      String contents = Files.lines(file.toPath()).collect(Collectors.joining());
+      log.info(String.format("Content of file %s:", file));
+      log.info(contents);
+    } catch (IOException e) {
+      log.severe(String.format("Could not read file %s", file));
+    }
+  }
+
+}
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..716d8d54e9644a7c9eaa2e0f0c6e9e09d8c9e474
--- /dev/null
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/StringBasedModelJoinWriterAcceptanceTests.java
@@ -0,0 +1,39 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.logging.Logger;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+import org.rosi_project.model_sync.model_join.representation.writer.TestedModels.Listing2;
+
+class StringBasedModelJoinWriterAcceptanceTests {
+
+  private static final Logger log = Logger.getLogger(StringBasedModelJoinWriterAcceptanceTests.class.getSimpleName());
+
+  /**
+   * @see TestedModels
+   */
+  @Test
+  void writeProducesListing2FromTechnicalReport() {
+    StringBasedModelJoinWriter writer = StringBasedModelJoinWriter.withNewlines();
+    String writerOutput = writer.write(Listing2.asModelJoin);
+
+    log.info("Writer output:");
+    log.info(writerOutput);
+
+    String sanitizedWriterOutput = sanitize(writerOutput);
+
+    String expectedOutput = sanitize(Listing2.asString);
+
+    // Although AssertJ provides an isEqualToIgnoringWhitespace as well as an
+    // isEqualToIgnoringNewline method, there's none that ignores both. Thus we reside to manual
+    // sanitization
+    assertThat(sanitizedWriterOutput).isEqualTo(expectedOutput);
+  }
+
+  private String sanitize(String rawModelJoin) {
+    return rawModelJoin.replaceAll("\\s", "");
+  }
+}
diff --git a/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java
new file mode 100644
index 0000000000000000000000000000000000000000..12ab7c144319e4a5a33174852cf402f2e22e644f
--- /dev/null
+++ b/src/test/java/org/rosi_project/model_sync/model_join/representation/writer/TestedModels.java
@@ -0,0 +1,69 @@
+package org.rosi_project.model_sync.model_join.representation.writer;
+
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.attributes;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.outgoing;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.supertype;
+import static org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder.thetaJoin;
+
+import org.rosi_project.model_sync.model_join.representation.core.AttributePath;
+import org.rosi_project.model_sync.model_join.representation.core.ClassResource;
+import org.rosi_project.model_sync.model_join.representation.grammar.ModelJoinExpression;
+import org.rosi_project.model_sync.model_join.representation.util.ModelJoinBuilder;
+
+class TestedModels {
+
+  /*
+   * The technical report which forms the basis of the tests here is
+   *
+   * Burger, E. et Al.: "ModelJoin - A Textual Domain-Specific Language for the Combination of
+   *    Heterogeneous Models"; Karslruhe Institute of Technololgy 2014
+   */
+
+  static class Listing2 {
+    static final ClassResource imdbFilm = ClassResource.from("imdb", "Film");
+    static final ClassResource libraryVideoCassette = ClassResource.from("library", "VideoCassette");
+    static final ClassResource libraryAudioVisualItem = ClassResource.from("library", "AudioVisualItem");
+    static final ClassResource jointargetMovie = ClassResource.from("jointarget", "Movie");
+    static final ClassResource jointargetVote = ClassResource.from("jointarget", "Vote");
+    static final ClassResource jointargetMediaItem = ClassResource.from("jointarget", "MediaItem");
+
+    static final AttributePath imdbFilmYear = AttributePath.from(imdbFilm, "year");
+    static final AttributePath imdbFilmVotes = AttributePath.from(imdbFilm, "votes");
+    static final AttributePath imdbVoteScore = AttributePath.from("imdb.Vote", "score");
+    static final AttributePath libraryAudioVisualItemMinutesLength = AttributePath.from(libraryAudioVisualItem, "minutesLength");
+
+    static final ModelJoinExpression asModelJoin = ModelJoinBuilder.createNewModelJoin()
+        .add(thetaJoin()
+            .join(imdbFilm)
+            .with(libraryVideoCassette)
+            .as(jointargetMovie)
+            .where(
+                "library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.firstname.concat(\" \") .concat(p.lastName) == a.name))")
+            .keep(attributes(imdbFilmYear))
+            .keep(outgoing(imdbFilmVotes)
+                .as(jointargetVote)
+                .keep(attributes(imdbVoteScore))
+                .buildExpression()
+            )
+            .keep(supertype(libraryAudioVisualItem)
+                .as(jointargetMediaItem)
+                .keep(attributes(libraryAudioVisualItemMinutesLength))
+                .buildExpression()
+            )
+            .done())
+        .build();
+
+    static final String asString = "theta join imdb.Film with library.VideoCassette as jointarget.Movie\n"
+        + "where library.VideoCassette.cast->forAll (p | imdb.Film.figures->playedBy->exists (a | p.\n"
+        + "firstname.concat(\" \") .concat(p.lastName) == a.name)) {\n"
+        + " keep attributes imdb.Film.year\n"
+        + " keep outgoing imdb.Film.votes as type jointarget.Vote {\n"
+        + " keep attributes imdb.Vote.score\n"
+        + "}\n"
+        + "keep supertype library.AudioVisualItem as type jointarget.MediaItem {\n"
+        + " keep attributes library.AudioVisualItem.minutesLength\n"
+        + "}\n"
+        + "}";
+  }
+
+}