From 80c5ccff89334ba897573575da437149bb37c848 Mon Sep 17 00:00:00 2001
From: Rico Bergmann <rico.bergmann1@tu-dresden.de>
Date: Wed, 17 Apr 2019 17:08:48 +0200
Subject: [PATCH] Implement the ModelJoin parser using ANTLR

---
 build.sbt                                     |   2 +
 project/build.properties                      |   2 +-
 src/main/antlr4/ModelJoin.g4                  |   6 +-
 .../grammar/KeepAggregateExpression.java      |   5 +
 .../grammar/KeepAttributesExpression.java     |   5 +
 .../grammar/KeepCalculatedExpression.java     |   5 +
 .../grammar/KeepExpression.java               |  45 +++
 .../grammar/KeepReferenceExpression.java      |  19 +-
 .../grammar/KeepSubTypeExpression.java        |   5 +
 .../grammar/KeepSuperTypeExpression.java      |   5 +
 .../parser/ModelJoinParser.java               | 162 +---------
 .../parser/ModelJoinParsingException.java     |  10 +-
 .../antlr/AntlrBackedModelJoinParser.java     | 104 +++++++
 .../parser/antlr/JoinStatementVisitor.java    | 174 +++++++++++
 .../parser/antlr/KeepStatementVisitor.java    | 293 ++++++++++++++++++
 .../parser/antlr/ModelJoinVisitor.java        |  28 ++
 .../parser/legacy/DefaultModelJoinParser.java | 187 +++++++++++
 .../parser/{ => legacy}/JoinParser.java       |   7 +-
 .../parser/{ => legacy}/KeepParser.java       |   3 +-
 .../representation/util/Assert.java           |   9 +
 20 files changed, 899 insertions(+), 177 deletions(-)
 create mode 100644 src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/AntlrBackedModelJoinParser.java
 create mode 100644 src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java
 create mode 100644 src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java
 create mode 100644 src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/ModelJoinVisitor.java
 create mode 100644 src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java
 rename src/main/java/org/rosi_project/model_sync/model_join/representation/parser/{ => legacy}/JoinParser.java (98%)
 rename src/main/java/org/rosi_project/model_sync/model_join/representation/parser/{ => legacy}/KeepParser.java (97%)

diff --git a/build.sbt b/build.sbt
index 0d7df12..408f0db 100644
--- a/build.sbt
+++ b/build.sbt
@@ -48,3 +48,5 @@ lazy val generator = (project in file("."))
   ).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
index 8db5ca2..cabf73b 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version = 1.2.1
\ No newline at end of file
+sbt.version = 1.2.7
diff --git a/src/main/antlr4/ModelJoin.g4 b/src/main/antlr4/ModelJoin.g4
index 864a32f..e5ceb0d 100644
--- a/src/main/antlr4/ModelJoin.g4
+++ b/src/main/antlr4/ModelJoin.g4
@@ -19,7 +19,7 @@ join : (naturaljoin | thetajoin | outerjoin) AS classres
   CLOSEDCURLYBRAKET )? ;
 naturaljoin : NATURAL JOIN classres WITH classres ;
 thetajoin : THETA JOIN classres WITH classres WHERE oclcond;
-outerjoin : (leftouterjoin | rightouterjoin) OUTER JOIN WITH classres;
+outerjoin : (leftouterjoin | rightouterjoin) OUTER JOIN classres WITH classres;
 leftouterjoin : LEFT ;
 rightouterjoin : RIGHT ;
 
@@ -34,8 +34,8 @@ keepexpr : (keeptypeexpr | keepoutgoingexpr | keepincomingexpr)
     keepexpr*
   CLOSEDCURLYBRAKET)? ;
 keeptypeexpr : keepsupertypeexpr | keepsubtypeexpr ;
-keepsupertypeexpr : KEEP SUPERTYPE ( AS TYPE classres )? ;
-keepsubtypeexpr : KEEP SUBTYPE ( AS TYPE classres )? ;
+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 )? ;
 
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
index ccef41e..d0da5aa 100644
--- 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
@@ -225,6 +225,11 @@ public class KeepAggregateExpression extends KeepExpression {
     return target;
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index 5078227..99cdb81 100644
--- 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
@@ -49,6 +49,11 @@ public class KeepAttributesExpression extends KeepExpression {
     return new ArrayList<>(attributes);
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index e028f41..8dbafba 100644
--- 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
@@ -94,6 +94,11 @@ public class KeepCalculatedExpression extends KeepExpression {
     return target;
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index 8fc8846..bd87900 100644
--- 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
@@ -1,5 +1,7 @@
 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>
@@ -10,6 +12,49 @@ package org.rosi_project.model_sync.model_join.representation.grammar;
  */
 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
index 7d35f41..b111ca3 100644
--- 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
@@ -5,6 +5,7 @@ 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?
@@ -56,7 +57,7 @@ public class KeepReferenceExpression extends KeepExpression {
 
     private ReferenceDirection referenceDirection;
     private AttributePath attribute;
-    private AttributePath target;
+    private ClassResource target;
 
     @Nonnull
     private List<KeepExpression> keeps;
@@ -92,7 +93,7 @@ public class KeepReferenceExpression extends KeepExpression {
      * Specifies the name of the attribute in the target join.
      */
     @Nonnull
-    public KeepReferenceBuilder as(@Nonnull AttributePath target) {
+    public KeepReferenceBuilder as(@Nonnull ClassResource target) {
       this.target = target;
       return this;
     }
@@ -131,7 +132,7 @@ public class KeepReferenceExpression extends KeepExpression {
   private final ReferenceDirection referenceDirection;
 
   @Nonnull
-  private final AttributePath target;
+  private final ClassResource target;
 
   @Nonnull
   private final List<KeepExpression> keeps;
@@ -142,14 +143,13 @@ public class KeepReferenceExpression extends KeepExpression {
    * @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 attribute under which the resulting references should be made
-   *     available
+   * @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 AttributePath target,
+      @Nonnull ClassResource target,
       @Nonnull List<KeepExpression> keeps) {
     this.attribute = attribute;
     this.referenceDirection = referenceDirection;
@@ -179,7 +179,7 @@ public class KeepReferenceExpression extends KeepExpression {
    * available.
    */
   @Nonnull
-  public AttributePath getTarget() {
+  public ClassResource getTarget() {
     return target;
   }
 
@@ -192,6 +192,11 @@ public class KeepReferenceExpression extends KeepExpression {
     return new ArrayList<>(keeps);
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index 11ceccb..fd73264 100644
--- 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
@@ -136,6 +136,11 @@ public class KeepSubTypeExpression extends KeepExpression {
     return new ArrayList<>(keeps);
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index 3b68024..703adef 100644
--- 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
@@ -135,6 +135,11 @@ public class KeepSuperTypeExpression extends KeepExpression {
     return new ArrayList<>(keeps);
   }
 
+  @Override
+  public void accept(@Nonnull KeepExpressionVisitor visitor) {
+    visitor.visit(this);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
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
index 93d6bf8..87c6f0c 100644
--- 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
@@ -1,33 +1,24 @@
 package org.rosi_project.model_sync.model_join.representation.parser;
 
-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.util.ModelJoinBuilder;
 
 /**
- * The {@code ModelJoinParser} reads a {@code ModelJoin} file from disk and creates matching
- * representation for it.
+ * The {@code ModelJoinParser} reads a {@code ModelJoin} file from disk and creates a matching
+ * logical representation for it.
  *
  * @author Rico Bergmann
  */
-public class ModelJoinParser {
+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
-  public static Optional<ModelJoinExpression> read(@Nonnull File modelFile) {
-    ModelJoinParser parser = new ModelJoinParser(modelFile, ErrorReportingStrategy.OPTIONAL);
-    return parser.run();
-  }
+  Optional<ModelJoinExpression> read(@Nonnull File modelFile);
 
   /**
    * Converts the {@code ModelJoin} file into a {@link ModelJoinExpression}. If parsing fails, a
@@ -36,149 +27,6 @@ public class ModelJoinParser {
    * @throws ModelJoinParsingException if the model may not be parsed for some reason.
    */
   @Nonnull
-  public static ModelJoinExpression readOrThrow(@Nonnull File modelFile) {
-    ModelJoinParser parser = new ModelJoinParser(modelFile, ErrorReportingStrategy.EXCEPTION);
-    Optional<ModelJoinExpression> result = parser.run();
+  ModelJoinExpression readOrThrow(@Nonnull File modelFile);
 
-    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 ModelJoinParser(@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/ModelJoinParsingException.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/ModelJoinParsingException.java
index 4a1067c..6b72c01 100644
--- 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
@@ -1,8 +1,8 @@
 package org.rosi_project.model_sync.model_join.representation.parser;
 
 /**
- * The {@code ModelJoinParsingException} indicates that a file could not be read into a
- * {@code ModelJoin} instance correctly.
+ * 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
  */
@@ -11,21 +11,21 @@ public class ModelJoinParsingException extends RuntimeException {
   /**
    * @see RuntimeException#RuntimeException(String)
    */
-  ModelJoinParsingException(String message) {
+  public ModelJoinParsingException(String message) {
     super(message);
   }
 
   /**
    * @see RuntimeException#RuntimeException(String, Throwable)
    */
-  ModelJoinParsingException(String message, Throwable cause) {
+  public ModelJoinParsingException(String message, Throwable cause) {
     super(message, cause);
   }
 
   /**
    * @see RuntimeException#RuntimeException(Throwable)
    */
-  ModelJoinParsingException(Throwable cause) {
+  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 0000000..8677378
--- /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 0000000..a00d25b
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/JoinStatementVisitor.java
@@ -0,0 +1,174 @@
+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.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.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.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 0000000..a7e6397
--- /dev/null
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/antlr/KeepStatementVisitor.java
@@ -0,0 +1,293 @@
+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.RelativeAttribute;
+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.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.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 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 0000000..f1883fa
--- /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/legacy/DefaultModelJoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/DefaultModelJoinParser.java
new file mode 100644
index 0000000..25f4b9c
--- /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/JoinParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java
similarity index 98%
rename from src/main/java/org/rosi_project/model_sync/model_join/representation/parser/JoinParser.java
rename to src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java
index 36b2de2..41f40e9 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/JoinParser.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/JoinParser.java
@@ -1,4 +1,4 @@
-package org.rosi_project.model_sync.model_join.representation.parser;
+package org.rosi_project.model_sync.model_join.representation.parser.legacy;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -10,6 +10,7 @@ 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;
@@ -19,7 +20,7 @@ import org.rosi_project.model_sync.model_join.representation.util.JoinFactory.Th
  * 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 ModelJoinParser} only as
+ * 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
@@ -50,7 +51,7 @@ class JoinParser {
   /**
    * Full constructor.
    *
-   * @param startingLine the line that was last read by the {@link ModelJoinParser} and should
+   * @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.
diff --git a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/KeepParser.java b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java
similarity index 97%
rename from src/main/java/org/rosi_project/model_sync/model_join/representation/parser/KeepParser.java
rename to src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java
index 1ea6db9..ee20f88 100644
--- a/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/KeepParser.java
+++ b/src/main/java/org/rosi_project/model_sync/model_join/representation/parser/legacy/KeepParser.java
@@ -1,4 +1,4 @@
-package org.rosi_project.model_sync.model_join.representation.parser;
+package org.rosi_project.model_sync.model_join.representation.parser.legacy;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -12,6 +12,7 @@ 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
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
index 08cff90..d72ecca 100644
--- 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
@@ -11,6 +11,15 @@ import javax.annotation.Nonnull;
  */
 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}.
    */
-- 
GitLab