Browse Source

Merge remote-tracking branch 'origin/master' into feature/metainf

# Conflicts:
#	jplag.cli/pom.xml
#	jplag/src/main/java/de/jplag/options/LanguageOption.java
Dominik Fuchß 4 years ago
parent
commit
58de5b844a
41 changed files with 1954 additions and 173 deletions
  1. 1 1
      README.md
  2. 8 0
      jplag.cli/pom.xml
  3. 42 0
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/FileUtil.java
  4. 37 0
      jplag.frontend-testutils/src/test/java/de/jplag/testutils/TokenUtils.java
  5. 16 0
      jplag.frontend-utils/src/main/java/de/jplag/Language.java
  6. 35 5
      jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java
  7. 43 0
      jplag.frontend.emf-metamodel-dynamic/pom.xml
  8. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java
  9. 60 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenConstants.java
  10. 34 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java
  11. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java
  12. 29 0
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java
  13. 61 0
      jplag.frontend.emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java
  14. 16 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStore.ecore
  15. 38 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreExtended.ecore
  16. 16 0
      jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreRenamed.ecore
  17. 39 0
      jplag.frontend.emf-metamodel/pom.xml
  18. 72 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java
  19. 70 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java
  20. 35 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenConstants.java
  21. 87 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java
  22. 130 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java
  23. 169 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java
  24. 75 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/EMFUtil.java
  25. 90 0
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java
  26. 66 0
      jplag.frontend.emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java
  27. 16 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStore.ecore
  28. 38 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreExtended.ecore
  29. 16 0
      jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreRenamed.ecore
  30. 2 2
      jplag.frontend.scala/pom.xml
  31. 8 0
      jplag/pom.xml
  32. 1 0
      jplag/src/main/java/de/jplag/SubmissionSetBuilder.java
  33. 10 3
      jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java
  34. 9 5
      jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java
  35. 16 0
      jplag/src/test/java/de/jplag/NormalComparisonTest.java
  36. 18 1
      pom.xml
  37. 427 108
      report-viewer/package-lock.json
  38. 3 3
      report-viewer/package.json
  39. 30 21
      report-viewer/src/components/CodePanel.vue
  40. 19 17
      report-viewer/src/components/MatchTable.vue
  41. 14 7
      report-viewer/src/views/FileUploadView.vue

+ 1 - 1
README.md

@@ -48,7 +48,7 @@ Usage: JPlag [ options ] [ <root-dir> ... ] [ -new <new-dir> ... ] [ -old <old-d
 
 named arguments:
   -h, --help       show this help message and exit
-  -l               {java,python3,cpp,csharp,golang,kotlin,rlang,scala,char,text,scheme} Select the language to parse the submissions (default: java)
+  -l               {java,python3,cpp,csharp,golang,kotlin,rlang,scala,char,text,scheme,emf-metamodel,emf-metamodel-dynamic} Select the language to parse the submissions (default: java)
   -bc BC           Path of the directory containing the base code (common framework used in all submissions)
   -v               {quiet,long} Verbosity of the logging (default: quiet)
   -d               Debug parser. Non-parsable files will be stored (default: false)

+ 8 - 0
jplag.cli/pom.xml

@@ -62,6 +62,14 @@
             <groupId>de.jplag</groupId>
             <artifactId>chars</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel-dynamic</artifactId>
+        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>endToEndTesting</artifactId>

+ 42 - 0
jplag.frontend-testutils/src/test/java/de/jplag/testutils/FileUtil.java

@@ -0,0 +1,42 @@
+package de.jplag.testutils;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.util.Arrays;
+
+/**
+ * Test utility regarding files and directories.
+ */
+public final class FileUtil {
+
+    private static final String CONTENT_NOT_FOUND = "Directory contents not as expected";
+    private static final String DIRECTORY_NOT_FOUND = "Could not find directory: ";
+
+    private FileUtil() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Checks if a directory exists and contains the expected files.
+     * @param directory is the directory to check.
+     * @param expectedFilesSorted is the sorted list (a to z) of expected file names.
+     */
+    public static void assertDirectory(File directory, String[] expectedFilesSorted) {
+        assertTrue(directory.exists(), DIRECTORY_NOT_FOUND + directory);
+        String[] filesInDirectory = directory.list();
+        Arrays.sort(filesInDirectory);
+        assertArrayEquals(expectedFilesSorted, filesInDirectory, CONTENT_NOT_FOUND);
+    }
+
+    /**
+     * Clears all files from a directory that whose names end in a certain suffix.
+     * @param directory is the target directory.
+     * @param suffix is the file suffix of the files to clear.
+     */
+    public static void clearFiles(File directory, String suffix) {
+        Arrays.stream(directory.listFiles()).filter(file -> file.getName().endsWith(suffix)).forEach(File::delete);
+    }
+
+}

+ 37 - 0
jplag.frontend-testutils/src/test/java/de/jplag/testutils/TokenUtils.java

@@ -0,0 +1,37 @@
+package de.jplag.testutils;
+
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+import de.jplag.Token;
+import de.jplag.TokenList;
+
+public final class TokenUtils {
+
+    private TokenUtils() {
+        // private constructor to prevent instantiation
+    }
+
+    /**
+     * Returns the type of all tokens in a {@link TokenList} that belong to a file.
+     * @param tokens is the {@link TokenList}.
+     * @param name is the name of the target file.
+     * @return the immutable list of token types.
+     */
+    public static List<Integer> tokenTypesByFile(TokenList tokenList, String name) {
+        var tokens = tokensByFile(tokenList, name);
+        return tokens.stream().map(Token::getType).toList();
+    }
+
+    /**
+     * Returns the tokens in a {@link TokenList} that belong to a file.
+     * @param tokens is the {@link TokenList}.
+     * @param name is the name of the target file.
+     * @return the immutable list of tokens.
+     */
+    public static List<Token> tokensByFile(TokenList tokenList, String name) {
+        var tokens = StreamSupport.stream(tokenList.allTokens().spliterator(), false);
+        return tokens.filter(it -> it.getFile().startsWith(name)).toList();
+    }
+
+}

+ 16 - 0
jplag.frontend-utils/src/main/java/de/jplag/Language.java

@@ -54,4 +54,20 @@ public interface Language {
     default boolean isPreformatted() {
         return true;
     }
+
+    /**
+     * Indicates whether the input files (code) should be used as representation in the report, or different files that form
+     * a view on the input files.
+     */
+    default boolean useViewFiles() {
+        return false;
+    }
+
+    /**
+     * If the language uses representation files, this method returns the suffix used for the representation files.
+     */
+    default String viewFileSuffix() {
+        return "";
+    }
+
 }

+ 35 - 5
jplag.frontend-utils/src/main/java/de/jplag/TokenPrinter.java

@@ -1,15 +1,16 @@
 package de.jplag;
 
-import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toMap;
 
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,6 +42,19 @@ public final class TokenPrinter {
         // Utility class, no public constructor.
     }
 
+    /**
+     * Creates a string representation of a set of files line by line and adds the tokens under the lines.
+     * @param tokens is the set of tokens parsed from the files.
+     * @param directory is the common directory of the files.
+     * @param fileNames is a collection of the file names.
+     * @param suffix is the optional view file suffix.
+     * @return the string representation.
+     */
+    public static String printTokens(TokenList tokens, File directory, Collection<String> fileNames, Optional<String> suffix) {
+        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
+        return printTokens(tokens, files, directory, suffix);
+    }
+
     /**
      * Creates a string representation of a set of files line by line and adds the tokens under the lines.
      * @param tokens is the set of tokens parsed from the files.
@@ -49,8 +63,8 @@ public final class TokenPrinter {
      * @return the string representation.
      */
     public static String printTokens(TokenList tokens, File directory, Collection<String> fileNames) {
-        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).collect(toList());
-        return printTokens(tokens, files, directory);
+        Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
+        return printTokens(tokens, files, directory, Optional.empty());
     }
 
     /**
@@ -60,6 +74,17 @@ public final class TokenPrinter {
      * @return the string representation.
      */
     public static String printTokens(TokenList tokens, Collection<File> files, File root) {
+        return printTokens(tokens, files, root, Optional.empty());
+    }
+
+    /**
+     * Creates a string representation of a collection of files line by line and adds the tokens under the lines.
+     * @param tokens is the set of tokens parsed from the files.
+     * @param files are the parsed files.
+     * @param suffix is the optional view file suffix.
+     * @return the string representation.
+     */
+    public static String printTokens(TokenList tokens, Collection<File> files, File root, Optional<String> suffix) {
         Map<String, List<String>> filesToLines = readFiles(files);
         StringBuilder builder = new StringBuilder();
         int lineIndex = 0;
@@ -84,6 +109,11 @@ public final class TokenPrinter {
                 columnIndex = 1;
 
                 String fileName = token.getFile().isEmpty() ? root.getName() : token.getFile();
+                fileName = fileName + suffix.orElse("");
+                if (!filesToLines.containsKey(fileName)) {
+                    throw new IllegalStateException("Cannot find file " + fileName + " in " + filesToLines.keySet());
+                }
+
                 currentLine = filesToLines.get(fileName).get(lineIndex - 1);
 
                 appendCodeLinePrefix(builder, lineIndex);
@@ -91,8 +121,6 @@ public final class TokenPrinter {
                 appendTokenLinePrefix(builder, lineIndex);
             }
 
-            assert currentLine != null;
-
             // New line if already past token index:
             if (columnIndex > token.getColumn()) {
                 appendTokenLinePrefix(builder, lineIndex);
@@ -190,6 +218,8 @@ public final class TokenPrinter {
     private static List<String> linesFromFile(File file) {
         try {
             return Files.readAllLines(file.toPath());
+        } catch (NoSuchFileException exception) {
+            logger.error("File does not exist, thus no tokens are printed: " + file.getAbsolutePath());
         } catch (IOException exception) {
             logger.error("Cannot read " + file.getAbsolutePath() + ":", exception);
         }

+ 43 - 0
jplag.frontend.emf-metamodel-dynamic/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>de.jplag</groupId>
+        <artifactId>aggregator</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>emf-metamodel-dynamic</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-testutils</artifactId>
+            <version>${revision}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore</artifactId>
+            <version>${emf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.common</artifactId>
+            <version>${emf.ecore.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+            <version>${emf.ecore.xmi.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelToken.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.MetamodelToken;
+
+/**
+ * EMF metamodel token which uses dynamically created tokens.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelToken extends MetamodelToken {
+
+    public DynamicMetamodelToken(int type, String file, EObject eObject) {
+        super(type, file, eObject);
+    }
+
+    public DynamicMetamodelToken(int type, String file) {
+        super(type, file);
+    }
+
+    @Override
+    protected String type2string() {
+        if (type < DynamicMetamodelTokenConstants.TOKEN_TYPE_START) {
+            return super.type2string();
+        }
+        return DynamicMetamodelTokenConstants.getTokenString(type);
+    }
+
+}

+ 60 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/DynamicMetamodelTokenConstants.java

@@ -0,0 +1,60 @@
+package de.jplag.emf.dynamic;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.emf.ecore.EClass;
+
+import de.jplag.TokenConstants;
+
+/**
+ * Utility class for the dynamic creation of token constants. Replaces a handcrafted token set.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelTokenConstants implements TokenConstants {
+    private static final String UNKNOWN_TOKEN = "<Unknown>";
+
+    public static final int TOKEN_TYPE_START = 2;
+
+    private static Map<EClass, Integer> eClassToTokenType = new HashMap<>();
+    private static Map<Integer, EClass> tokenTypeToEClass = new HashMap<>();
+    private static int tokenTypeIndex = TOKEN_TYPE_START;
+
+    private DynamicMetamodelTokenConstants() {
+        // private constructor for non-instantiability.
+    }
+
+    public static int getTokenType(EClass eClass) {
+        if (eClassToTokenType.containsKey(eClass)) {
+            return eClassToTokenType.get(eClass);
+        }
+        Integer tokenType = tokenTypeIndex++;
+        eClassToTokenType.put(eClass, tokenType);
+        tokenTypeToEClass.put(tokenType, eClass);
+        return tokenType;
+    }
+
+    public static String getTokenString(int tokenType) {
+        if (tokenTypeToEClass.containsKey(tokenType)) {
+            return tokenTypeToEClass.get(tokenType).getName();
+        }
+        return UNKNOWN_TOKEN;
+    }
+
+    /**
+     * Returns the current size of the dynamic token set.
+     * @return the number of known tokens.
+     */
+    public static int getNumberOfTokens() {
+        return eClassToTokenType.size();
+    }
+
+    /**
+     * @return a read only view on the collection of token strings.
+     */
+    public static Collection<String> getTokenStrings() {
+        return eClassToTokenType.keySet().stream().map(EClass::getName).toList();
+    }
+
+}

+ 34 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java

@@ -0,0 +1,34 @@
+package de.jplag.emf.dynamic;
+
+import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
+
+/**
+ * Language for EMF metamodels from the Eclipse Modeling Framework (EMF). This language is based on a dynamically
+ * created token set instead of a hand-picked one.
+ * @author Timur Saglam
+ */
+public class Language extends de.jplag.emf.Language {
+    private static final String NAME = "EMF metamodels (dynamically created token set)";
+    private static final String SHORT_NAME = "EMF metamodel (dynamic)";
+
+    private static final int DEFAULT_MIN_TOKEN_MATCH = 10;
+
+    public Language() {
+        super(new DynamicEcoreParser());
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public String getShortName() {
+        return SHORT_NAME;
+    }
+
+    @Override
+    public int minimumTokenMatch() {
+        return DEFAULT_MIN_TOKEN_MATCH;
+    }
+}

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicEcoreParser.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic.parser;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.MetamodelToken;
+import de.jplag.emf.dynamic.DynamicMetamodelToken;
+import de.jplag.emf.parser.EcoreParser;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Parser for EMF metamodels based on dynamically created tokens.
+ * @author Timur Saglam
+ */
+public class DynamicEcoreParser extends EcoreParser {
+
+    private static final String NO_PREFIX = "";
+
+    @Override
+    protected AbstractMetamodelVisitor createMetamodelVisitor() {
+        return new DynamicMetamodelTokenGenerator(this);
+    }
+
+    @Override
+    public void addToken(int type, EObject source) {
+        MetamodelToken token = new DynamicMetamodelToken(type, currentFile, source);
+        treeView.addToken(token, visitor.getCurrentTreeDepth(), NO_PREFIX);
+        tokens.addToken(token);
+    }
+}

+ 29 - 0
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/parser/DynamicMetamodelTokenGenerator.java

@@ -0,0 +1,29 @@
+package de.jplag.emf.dynamic.parser;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.emf.dynamic.DynamicMetamodelTokenConstants;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Visits a metamodel containment tree and extracts the relevant token based on a dynamically created token set.
+ * @author Timur Saglam
+ */
+public class DynamicMetamodelTokenGenerator extends AbstractMetamodelVisitor {
+    private final DynamicEcoreParser parser;
+
+    /**
+     * Creates the visitor.
+     * @param parser is the parser which receives the generated tokens.
+     */
+    public DynamicMetamodelTokenGenerator(DynamicEcoreParser parser) {
+        super(false);
+        this.parser = parser;
+    }
+
+    @Override
+    protected void visitEObject(EObject eObject) {
+        int tokenType = DynamicMetamodelTokenConstants.getTokenType(eObject.eClass());
+        parser.addToken(tokenType, eObject);
+    }
+}

+ 61 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/java/de/jplag/emf/dynamic/MinimalDynamicMetamodelTest.java

@@ -0,0 +1,61 @@
+package de.jplag.emf.dynamic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.FileUtil;
+import de.jplag.testutils.TokenUtils;
+
+class MinimalDynamicMetamodelTest {
+    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
+
+    private static final Path BASE_PATH = Path.of("src", "test", "resources", "de", "jplag", "models");
+    private static final String[] TEST_SUBJECTS = {"bookStore.ecore", "bookStoreExtended.ecore", "bookStoreRenamed.ecore"};
+
+    private de.jplag.Language frontend;
+    private File baseDirectory;
+
+    @BeforeEach
+    public void setUp() {
+        frontend = new Language();
+        baseDirectory = BASE_PATH.toFile();
+        FileUtil.assertDirectory(baseDirectory, TEST_SUBJECTS);
+    }
+
+    @Test
+    void testBookstoreMetamodels() {
+        TokenList result = frontend.parse(baseDirectory, TEST_SUBJECTS);
+        List<String> treeViewFiles = Arrays.stream(TEST_SUBJECTS).map(it -> it + Language.VIEW_FILE_SUFFIX).toList();
+        logger.debug(TokenPrinter.printTokens(result, baseDirectory, treeViewFiles, Optional.of(Language.VIEW_FILE_SUFFIX)));
+        logger.info(("Dynamic token set: " + DynamicMetamodelTokenConstants.getTokenStrings()));
+        logger.info("parsed tokens: " + result.allTokens().toString());
+        assertEquals(7, DynamicMetamodelTokenConstants.getTokenStrings().size());
+        assertEquals(64, result.size());
+
+        var bookstoreTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[0]);
+        var bookstoreRenamedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[2]);
+        var bookstoreExtendedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[1]);
+        assertTrue(bookstoreTokens.size() < bookstoreExtendedTokens.size());
+        assertIterableEquals(bookstoreTokens, bookstoreRenamedTokens);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX);
+    }
+}

+ 16 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStore.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="owner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+        eType="#//Book" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="Book">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 38 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreExtended.ecore

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eSubpackages name="store" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/store"
+      nsPrefix="store">
+    <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+          eType="#//store/Book" containment="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//person/Person"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Book">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+          iD="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="author" eType="#//person/Author"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="genre" eType="#//store/Genre"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EEnum" name="Genre">
+      <eLiterals name="NOVEL"/>
+      <eLiterals name="COOKBOOK" value="1"/>
+      <eLiterals name="BIOGRAPHY" value="3"/>
+      <eLiterals name="TEXTBOOK" value="4"/>
+    </eClassifiers>
+  </eSubpackages>
+  <eSubpackages name="person" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/person"
+      nsPrefix="person">
+    <eClassifiers xsi:type="ecore:EClass" name="Author" eSuperTypes="#//person/Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isStageName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="firstName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="lastName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    </eClassifiers>
+  </eSubpackages>
+</ecore:EPackage>

+ 16 - 0
jplag.frontend.emf-metamodel-dynamic/src/test/resources/de/jplag/models/bookStoreRenamed.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="StoreThatSellsBooks">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="nameOfOwner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="city" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="soldItems" upperBound="-1"
+        eType="#//BookStoreItem" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="BookStoreItem">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="identifier" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 39 - 0
jplag.frontend.emf-metamodel/pom.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>de.jplag</groupId>
+        <artifactId>aggregator</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>emf-metamodel</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>frontend-testutils</artifactId>
+            <version>${revision}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore</artifactId>
+            <version>${emf.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.common</artifactId>
+            <version>${emf.ecore.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.emf</groupId>
+            <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+            <version>${emf.ecore.xmi.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 72 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java

@@ -0,0 +1,72 @@
+package de.jplag.emf;
+
+import java.io.File;
+import java.util.Arrays;
+
+import org.eclipse.emf.ecore.EcorePackage;
+
+import de.jplag.TokenList;
+import de.jplag.emf.parser.EcoreParser;
+
+/**
+ * Language for EMF metamodels from the Eclipse Modeling Framework (EMF).
+ * @author Timur Saglam
+ */
+public class Language implements de.jplag.Language {
+    public static final String VIEW_FILE_SUFFIX = ".TreeView";
+    public static final String FILE_ENDING = "." + EcorePackage.eNAME;
+
+    private static final String NAME = "EMF metamodel";
+    private static final String SHORT_NAME = "EMF metamodel";
+    private static final int DEFAULT_MIN_TOKEN_MATCH = 6;
+
+    protected final EcoreParser parser;
+
+    public Language() {
+        this(new EcoreParser());
+    }
+
+    protected Language(EcoreParser parser) {
+        this.parser = parser;
+    }
+
+    @Override
+    public String[] suffixes() {
+        return new String[] {FILE_ENDING};
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public String getShortName() {
+        return SHORT_NAME;
+    }
+
+    @Override
+    public int minimumTokenMatch() {
+        return DEFAULT_MIN_TOKEN_MATCH;
+    }
+
+    @Override
+    public TokenList parse(File dir, String[] files) {
+        return parser.parse(dir, Arrays.asList(files));
+    }
+
+    @Override
+    public boolean hasErrors() {
+        return parser.hasErrors();
+    }
+
+    @Override
+    public boolean useViewFiles() {
+        return true;
+    }
+
+    @Override
+    public String viewFileSuffix() {
+        return VIEW_FILE_SUFFIX;
+    }
+}

+ 70 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelToken.java

@@ -0,0 +1,70 @@
+package de.jplag.emf;
+
+import java.util.Optional;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.Token;
+import de.jplag.TokenConstants;
+
+/**
+ * EMF metamodel token.
+ * @author Timur Saglam
+ */
+public class MetamodelToken extends Token implements MetamodelTokenConstants {
+
+    private final Optional<EObject> eObject;
+
+    /**
+     * Creates an Ecore metamodel token that corresponds to an EObject.
+     * @param type is the corresponding ID of the {@link TokenConstants}.
+     * @param file is the name of the source model file.
+     * @param eObject is the corresponding eObject in the model from which this token was extracted.
+     */
+    public MetamodelToken(int type, String file, EObject eObject) {
+        super(type, file, -1);
+        this.eObject = Optional.of(eObject);
+    }
+
+    /**
+     * Creates an Ecore metamodel token.
+     * @param type is the corresponding ID of the {@link TokenConstants}.
+     * @param file is the name of the source model file.
+     */
+    public MetamodelToken(int type, String file) {
+        super(type, file, -1);
+        this.eObject = Optional.empty();
+    }
+
+    /**
+     * @return the optional corresponding EObject of the token.
+     */
+    public Optional<EObject> getEObject() {
+        return eObject;
+    }
+
+    @Override
+    protected String type2string() {
+        return switch (type) {
+            case PACKAGE -> "EPackage";
+            case ANNOTATION -> "EAnnotation";
+            case CLASS -> "EClass";
+            case DATATYPE -> "EDatatype";
+            case ENUM -> "EEnum";
+            case ENUM_LITERAL -> "EEnumLiteral";
+            case OPERATION -> "EOperation";
+            case REFERENCE -> "EReference";
+            case ATTRIBUTE -> "EAttribute";
+            case PARAMETER -> "EParameter";
+            case INTERFACE -> "EClass (Interface)";
+            case SUPER_TYPE -> "ESuperType";
+            case ID_ATTRIBUTE -> "EAttribute (ID)";
+            case CONTAINMENT -> "EReference (Containment)";
+            case ABSTRACT_CLASS -> "EClass (Abstract)";
+            case RETURN_TYPE -> "EClassifier (Return Type)";
+            case THROWS_DECLARATION -> "EClassifier (Exception)";
+            case FILE_END -> "(EOF)";
+            default -> "<UNKNOWN" + type + ">";
+        };
+    }
+}

+ 35 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/MetamodelTokenConstants.java

@@ -0,0 +1,35 @@
+package de.jplag.emf;
+
+import de.jplag.TokenConstants;
+
+/**
+ * Ecore meta-metamodel token constants. They define which tokens are extracted from a metamodel.
+ * @author Timur Saglam
+ */
+public interface MetamodelTokenConstants extends TokenConstants {
+
+    int PACKAGE = 2;
+    int ANNOTATION = 3;
+    int CLASS = 4;
+    int DATATYPE = 5;
+    int ENUM = 6;
+    int ENUM_LITERAL = 7;
+    int OPERATION = 8;
+    int REFERENCE = 9;
+    int ATTRIBUTE = 10;
+    int PARAMETER = 11;
+    int INTERFACE = 12;
+    int SUPER_TYPE = 13;
+    int ID_ATTRIBUTE = 14;
+    int CONTAINMENT = 15;
+    int ABSTRACT_CLASS = 16;
+    int RETURN_TYPE = 17;
+    int THROWS_DECLARATION = 18;
+    int TYPE_PARAMETER = 19;
+    int BOUND = 20;
+
+    /*
+     * Number of token constants:
+     */
+    int NUM_DIFF_TOKENS = 21;
+}

+ 87 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/EcoreParser.java

@@ -0,0 +1,87 @@
+package de.jplag.emf.parser;
+
+import java.io.File;
+import java.util.List;
+
+import org.eclipse.emf.ecore.EObject;
+
+import de.jplag.AbstractParser;
+import de.jplag.TokenConstants;
+import de.jplag.TokenList;
+import de.jplag.emf.Language;
+import de.jplag.emf.MetamodelToken;
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+import de.jplag.emf.util.EMFUtil;
+import de.jplag.emf.util.MetamodelTreeView;
+
+/**
+ * Parser for EMF metamodels.
+ * @author Timur Saglam
+ */
+public class EcoreParser extends AbstractParser {
+    protected TokenList tokens;
+    protected String currentFile;
+    protected MetamodelTreeView treeView;
+    protected AbstractMetamodelVisitor visitor;
+
+    /**
+     * Creates the parser.
+     */
+    public EcoreParser() {
+        EMFUtil.registerEcoreExtension();
+    }
+
+    /**
+     * Parses all tokens form a list of files.
+     * @param directory is the base directory.
+     * @param fileNames is the list of file names.
+     * @return the list of parsed tokens.
+     */
+    public TokenList parse(File directory, List<String> fileNames) {
+        errors = 0;
+        tokens = new TokenList();
+        for (String fileName : fileNames) {
+            currentFile = fileName;
+            String filePath = fileName.isEmpty() ? directory.toString() : directory.toString() + File.separator + fileName;
+            parseModelFile(filePath);
+        }
+        return tokens;
+    }
+
+    /**
+     * Loads a metamodel from a file and parses it.
+     * @param filePath is the path to the metamodel file.
+     */
+    protected void parseModelFile(String filePath) {
+        treeView = new MetamodelTreeView(filePath);
+        List<EObject> model = EMFUtil.loadModel(filePath);
+        if (model == null) {
+            errors++;
+        } else {
+            for (EObject root : model) {
+                visitor = createMetamodelVisitor();
+                visitor.visit(root);
+            }
+            tokens.addToken(new MetamodelToken(TokenConstants.FILE_END, currentFile));
+            treeView.writeToFile(Language.VIEW_FILE_SUFFIX);
+        }
+    }
+
+    /**
+     * Extension point for subclasses to employ different token generators.
+     * @return a token generating metamodel visitor.
+     */
+    protected AbstractMetamodelVisitor createMetamodelVisitor() {
+        return new MetamodelTokenGenerator(this);
+    }
+
+    public void addToken(int type, EObject source, String prefix) {
+        MetamodelToken token = new MetamodelToken(type, currentFile, source);
+        treeView.addToken(token, visitor.getCurrentTreeDepth(), prefix);
+        tokens.addToken(token);
+    }
+
+    public void addToken(int type, EObject source) {
+        addToken(type, source, "");
+    }
+}

+ 130 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/parser/MetamodelTokenGenerator.java

@@ -0,0 +1,130 @@
+package de.jplag.emf.parser;
+
+import static de.jplag.emf.MetamodelTokenConstants.ABSTRACT_CLASS;
+import static de.jplag.emf.MetamodelTokenConstants.ANNOTATION;
+import static de.jplag.emf.MetamodelTokenConstants.ATTRIBUTE;
+import static de.jplag.emf.MetamodelTokenConstants.BOUND;
+import static de.jplag.emf.MetamodelTokenConstants.CLASS;
+import static de.jplag.emf.MetamodelTokenConstants.CONTAINMENT;
+import static de.jplag.emf.MetamodelTokenConstants.DATATYPE;
+import static de.jplag.emf.MetamodelTokenConstants.ENUM;
+import static de.jplag.emf.MetamodelTokenConstants.ENUM_LITERAL;
+import static de.jplag.emf.MetamodelTokenConstants.ID_ATTRIBUTE;
+import static de.jplag.emf.MetamodelTokenConstants.INTERFACE;
+import static de.jplag.emf.MetamodelTokenConstants.OPERATION;
+import static de.jplag.emf.MetamodelTokenConstants.PACKAGE;
+import static de.jplag.emf.MetamodelTokenConstants.PARAMETER;
+import static de.jplag.emf.MetamodelTokenConstants.REFERENCE;
+import static de.jplag.emf.MetamodelTokenConstants.RETURN_TYPE;
+import static de.jplag.emf.MetamodelTokenConstants.SUPER_TYPE;
+import static de.jplag.emf.MetamodelTokenConstants.THROWS_DECLARATION;
+import static de.jplag.emf.MetamodelTokenConstants.TYPE_PARAMETER;
+
+import org.eclipse.emf.ecore.EAnnotation;
+import org.eclipse.emf.ecore.EAttribute;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EDataType;
+import org.eclipse.emf.ecore.EEnum;
+import org.eclipse.emf.ecore.EEnumLiteral;
+import org.eclipse.emf.ecore.EOperation;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EParameter;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.ETypeParameter;
+
+import de.jplag.emf.util.AbstractMetamodelVisitor;
+
+/**
+ * Visits a metamodel containment tree and extracts the relevant token.
+ * @author Timur Saglam
+ */
+public class MetamodelTokenGenerator extends AbstractMetamodelVisitor {
+    private EcoreParser parser;
+
+    /**
+     * Creates the visitor.
+     * @param parser is the parser which receives the generated tokens.
+     */
+    public MetamodelTokenGenerator(EcoreParser parser) {
+        super(true);
+        this.parser = parser;
+    }
+
+    @Override
+    protected void visitEAnnotation(EAnnotation eAnnotation) {
+        parser.addToken(ANNOTATION, eAnnotation);
+    }
+
+    @Override
+    protected void visitEAttribute(EAttribute eAttribute) {
+        if (eAttribute.isID()) {
+            parser.addToken(ID_ATTRIBUTE, eAttribute);
+        } else {
+            parser.addToken(ATTRIBUTE, eAttribute);
+        }
+    }
+
+    @Override
+    protected void visitEClass(EClass eClass) {
+        if (eClass.isInterface()) {
+            parser.addToken(INTERFACE, eClass);
+        } else if (eClass.isAbstract()) {
+            parser.addToken(ABSTRACT_CLASS, eClass);
+        } else {
+            parser.addToken(CLASS, eClass);
+        }
+        eClass.getESuperTypes().forEach(it -> parser.addToken(SUPER_TYPE, it, " extends "));
+    }
+
+    @Override
+    protected void visitEDataType(EDataType eDataType) {
+        if (!(eDataType instanceof EEnum)) {
+            parser.addToken(DATATYPE, eDataType);
+        }
+    }
+
+    @Override
+    protected void visitEEnum(EEnum eEnum) {
+        parser.addToken(ENUM, eEnum);
+    }
+
+    @Override
+    protected void visitEEnumLiteral(EEnumLiteral eEnumLiteral) {
+        parser.addToken(ENUM_LITERAL, eEnumLiteral);
+    }
+
+    @Override
+    protected void visitEOperation(EOperation eOperation) {
+        parser.addToken(OPERATION, eOperation);
+        if (eOperation.getEType() != null) {
+            parser.addToken(RETURN_TYPE, eOperation);
+        }
+        eOperation.getEExceptions().forEach(it -> parser.addToken(THROWS_DECLARATION, it));
+    }
+
+    @Override
+    protected void visitEPackage(EPackage ePackage) {
+        parser.addToken(PACKAGE, ePackage);
+    }
+
+    @Override
+    protected void visitEParameter(EParameter eParameter) {
+        parser.addToken(PARAMETER, eParameter);
+    }
+
+    @Override
+    protected void visitEReference(EReference eReference) {
+        if (eReference.isContainment()) {
+            parser.addToken(CONTAINMENT, eReference);
+        } else {
+            parser.addToken(REFERENCE, eReference);
+        }
+    }
+
+    @Override
+    protected void visitETypeParameter(ETypeParameter eTypeParameter) {
+        parser.addToken(TYPE_PARAMETER, eTypeParameter);
+        eTypeParameter.getEBounds().forEach(it -> parser.addToken(BOUND, it));
+    }
+
+}

+ 169 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/AbstractMetamodelVisitor.java

@@ -0,0 +1,169 @@
+package de.jplag.emf.util;
+
+import java.util.ArrayList;
+
+import org.eclipse.emf.ecore.EAnnotation;
+import org.eclipse.emf.ecore.EAttribute;
+import org.eclipse.emf.ecore.EClass;
+import org.eclipse.emf.ecore.EClassifier;
+import org.eclipse.emf.ecore.EDataType;
+import org.eclipse.emf.ecore.EEnum;
+import org.eclipse.emf.ecore.EEnumLiteral;
+import org.eclipse.emf.ecore.EFactory;
+import org.eclipse.emf.ecore.EGenericType;
+import org.eclipse.emf.ecore.ENamedElement;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EOperation;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EParameter;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.ETypeParameter;
+
+/**
+ * Visitor for the containment tree of an EMF Metamodel.
+ * @author Timur Saglam
+ */
+public abstract class AbstractMetamodelVisitor {
+
+    private final boolean sortContainmentsByType;
+
+    protected AbstractMetamodelVisitor(boolean sortContainmentsByType) {
+        this.sortContainmentsByType = sortContainmentsByType;
+    }
+
+    private int currentTreeDepth;
+
+    /**
+     * Returns the current depth in the containment tree from the starting point.
+     * @return the depth in tree node levels.
+     */
+    public int getCurrentTreeDepth() {
+        return currentTreeDepth;
+    }
+
+    /**
+     * Visits an EObject and all nodes in the containment tree below. Note that multiple visitor method may be called for a
+     * single element. For example <code>visitEClass()</code> and <code>visitEObject()</code>.
+     * @param eObject is the EObject to visit.
+     */
+    public final void visit(EObject eObject) {
+        visitEObject(eObject);
+        if (eObject instanceof EPackage ePackage) {
+            visitEPackage(ePackage);
+        }
+        if (eObject instanceof EAnnotation eAnnotation) {
+            visitEAnnotation(eAnnotation);
+        }
+        if (eObject instanceof EClassifier eClassifier) {
+            visitEClassifier(eClassifier);
+        }
+        if (eObject instanceof EClass eClass) {
+            visitEClass(eClass);
+        }
+        if (eObject instanceof EDataType eDataType) {
+            visitEDataType(eDataType);
+        }
+        if (eObject instanceof EStructuralFeature eStructuralFeature) {
+            visitEStructuralFeature(eStructuralFeature);
+        }
+        if (eObject instanceof EAttribute eAttribute) {
+            visitEAttribute(eAttribute);
+        }
+        if (eObject instanceof EReference eReference) {
+            visitEReference(eReference);
+        }
+        if (eObject instanceof EOperation eOperation) {
+            visitEOperation(eOperation);
+        }
+        if (eObject instanceof EEnum eEnum) {
+            visitEEnum(eEnum);
+        }
+        if (eObject instanceof EEnumLiteral eEnumLiteral) {
+            visitEEnumLiteral(eEnumLiteral);
+        }
+        if (eObject instanceof EFactory eFactory) {
+            visitEFactory(eFactory);
+        }
+        if (eObject instanceof EParameter eParameter) {
+            visitEParameter(eParameter);
+        }
+        if (eObject instanceof EGenericType eGenericType) {
+            visitEGenericType(eGenericType);
+        }
+        if (eObject instanceof ETypeParameter eTypeParameter) {
+            visitETypeParameter(eTypeParameter);
+        }
+        if (eObject instanceof ENamedElement eNamedElement) {
+            visitENamedElement(eNamedElement);
+        }
+
+        var children = new ArrayList<>(eObject.eContents());
+        if (sortContainmentsByType) {
+            children.sort((first, second) -> first.eClass().getName().compareTo(second.eClass().getName()));
+        }
+
+        currentTreeDepth++;
+        for (EObject child : children) {
+            visit(child);
+        }
+        currentTreeDepth--;
+    }
+
+    protected void visitENamedElement(ENamedElement eNamedElement) {
+    }
+
+    /**
+     * Visit method that gets called for all annotation nodes.
+     * @param eAnnotation is the node that is visited.
+     */
+    protected void visitEAnnotation(EAnnotation eAnnotation) {
+    }
+
+    protected void visitEAttribute(EAttribute eAttribute) {
+    }
+
+    protected void visitEClass(EClass eClass) {
+    }
+
+    protected void visitEClassifier(EClassifier eClassifier) {
+    }
+
+    protected void visitEDataType(EDataType eDataType) {
+    }
+
+    protected void visitEEnum(EEnum eEnum) {
+    }
+
+    protected void visitEEnumLiteral(EEnumLiteral eEnumLiteral) {
+    }
+
+    protected void visitEFactory(EFactory eFactory) {
+    }
+
+    protected void visitEGenericType(EGenericType eGenericType) {
+    }
+
+    protected void visitEObject(EObject eObject) {
+
+    }
+
+    protected void visitEOperation(EOperation eOperation) {
+    }
+
+    protected void visitEPackage(EPackage ePackage) {
+    }
+
+    protected void visitEParameter(EParameter eParameter) {
+    }
+
+    protected void visitEReference(EReference eReference) {
+    }
+
+    protected void visitEStructuralFeature(EStructuralFeature eStructuralFeature) {
+    }
+
+    protected void visitETypeParameter(ETypeParameter eTypeParameter) {
+    }
+
+}

+ 75 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/EMFUtil.java

@@ -0,0 +1,75 @@
+package de.jplag.emf.util;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.emf.common.util.URI;
+import org.eclipse.emf.common.util.WrappedException;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EPackage;
+import org.eclipse.emf.ecore.EcorePackage;
+import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.emf.ecore.resource.ResourceSet;
+import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for EMF specific functionality.
+ * @author Timur Saglam
+ */
+public final class EMFUtil {
+
+    private static final Logger logger = LoggerFactory.getLogger(EMFUtil.class);
+
+    private EMFUtil() {
+        // private constructor for non-instantiability.
+    }
+
+    /**
+     * Registers a file extension in the EMF registry for a XMI resource factory.
+     * @param extension is the file extension string including a dot. Use "*" for any extension.
+     */
+    public static void registerModelExtension(String extension) {
+        final Resource.Factory.Registry registry = Resource.Factory.Registry.INSTANCE;
+        final Map<String, Object> extensionMap = registry.getExtensionToFactoryMap();
+        extensionMap.put(extension, new XMIResourceFactoryImpl());
+    }
+
+    /**
+     * Register the Ecore file extension in the EMF registry.
+     */
+    public static void registerEcoreExtension() {
+        EcorePackage.eINSTANCE.eClass();
+        registerModelExtension(EcorePackage.eNAME);
+    }
+
+    /**
+     * Registers a collection of EPackages in the EPackage registry via their URIs.
+     * @param ePackages are the EPackages to register.
+     */
+    public static void registerEPackageURIs(Collection<EPackage> ePackages) {
+        ePackages.forEach(it -> EPackage.Registry.INSTANCE.put(it.getNsURI(), it));
+    }
+
+    /**
+     * Loads a model or metamodel from a absolute file path.
+     * @param filePath is the absolute path to the (meta)model.
+     * @return the content of the loaded (meta)model resource or null if it could not be loaded.
+     */
+    public static List<EObject> loadModel(String filePath) {
+        final ResourceSet resourceSet = new ResourceSetImpl();
+        try {
+            final Resource resource = resourceSet.getResource(URI.createFileURI(filePath), true);
+            return resource.getContents();
+        } catch (WrappedException exception) {
+            String name = new File(filePath).getName();
+            logger.error("Could not load " + name + ": " + exception.getCause().getMessage());
+        }
+        return null;
+    }
+
+}

+ 90 - 0
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/util/MetamodelTreeView.java

@@ -0,0 +1,90 @@
+package de.jplag.emf.util;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import org.eclipse.emf.ecore.ENamedElement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.emf.MetamodelToken;
+
+/**
+ * Simplistic tree view representation of an EMF metamodel.
+ * @author Timur Saglam
+ */
+public class MetamodelTreeView {
+    private final String filePath;
+    private int lineIndex;
+    private int columnIndex;
+    private final StringBuilder viewBuilder;
+    private static final String INDENTATION = "  ";
+    public final Logger logger;
+
+    /**
+     * Creates a tree view for a metamodel.
+     * @param filePath is the path to the file where the metamodel is persisted.
+     */
+    public MetamodelTreeView(String filePath) {
+        this.filePath = filePath;
+        logger = LoggerFactory.getLogger(this.getClass());
+        viewBuilder = new StringBuilder();
+    }
+
+    /**
+     * Adds a token to the view, thus adding the index information to the token.
+     * @param token is the token to add.
+     * @param treeDepth is the current containment tree depth, required for the indentation.
+     */
+    public void addToken(MetamodelToken token, int treeDepth, String prefix) {
+        token.getEObject().ifPresent(it -> {
+            if (prefix.isEmpty() && treeDepth > 0) {
+                lineIndex++;
+                columnIndex = 0;
+                viewBuilder.append(System.lineSeparator());
+            }
+
+            String tokenText = token.toString();
+            if (it instanceof ENamedElement element) {
+                tokenText = element.getName() + " : " + tokenText;
+            }
+            token.setLength(tokenText.length());
+
+            if (prefix.isEmpty()) {
+                for (int i = 0; i < treeDepth; i++) {
+                    viewBuilder.append(INDENTATION);
+                    columnIndex += INDENTATION.length();
+                }
+                viewBuilder.append(tokenText);
+            } else {
+                viewBuilder.append(prefix + tokenText);
+                columnIndex += prefix.length();
+            }
+
+            token.setLine(lineIndex + 1);
+            token.setColumn(columnIndex + 1);
+
+            columnIndex += tokenText.length();
+
+        });
+    }
+
+    /**
+     * Writes the tree view into a file.
+     * @param suffix is the suffix of the file to be written.
+     */
+    public void writeToFile(String suffix) {
+        File treeViewFile = new File(filePath + suffix);
+        try (BufferedWriter writer = new BufferedWriter(new FileWriter(treeViewFile));) {
+            if (!treeViewFile.createNewFile()) {
+                logger.warn("Overwriting tree view file: {}", treeViewFile);
+            }
+            writer.append(viewBuilder.toString());
+        } catch (IOException exception) {
+            logger.error("Could not write tree view file!", exception);
+        }
+    }
+
+}

+ 66 - 0
jplag.frontend.emf-metamodel/src/test/java/de/jplag/emf/MinimalMetamodelTest.java

@@ -0,0 +1,66 @@
+package de.jplag.emf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.jplag.TokenList;
+import de.jplag.TokenPrinter;
+import de.jplag.testutils.FileUtil;
+import de.jplag.testutils.TokenUtils;
+
+class MinimalMetamodelTest {
+    private final Logger logger = LoggerFactory.getLogger("JPlag-Test");
+
+    private static final Path BASE_PATH = Path.of("src", "test", "resources", "de", "jplag", "models");
+    private static final String[] TEST_SUBJECTS = {"bookStore.ecore", "bookStoreExtended.ecore", "bookStoreRenamed.ecore"};
+
+    private de.jplag.Language frontend;
+    private File baseDirectory;
+
+    @BeforeEach
+    public void setUp() {
+        frontend = new Language();
+        baseDirectory = BASE_PATH.toFile();
+        FileUtil.assertDirectory(baseDirectory, TEST_SUBJECTS);
+    }
+
+    @Test
+    void testBookstoreMetamodels() {
+        TokenList result = frontend.parse(baseDirectory, TEST_SUBJECTS);
+        List<String> treeViewFiles = Arrays.stream(TEST_SUBJECTS).map(it -> it + Language.VIEW_FILE_SUFFIX).toList();
+
+        logger.debug(TokenPrinter.printTokens(result, baseDirectory, treeViewFiles, Optional.of(Language.VIEW_FILE_SUFFIX)));
+        Field[] fields = MetamodelTokenConstants.class.getFields();
+        var constants = Arrays.stream(fields).map(Field::getName).filter(it -> !it.equals("NUM_DIFF_TOKENS")).toList();
+        logger.info(("Handcrafted token set: " + constants));
+        logger.info("Parsed tokens: " + result.allTokens().toString());
+        assertEquals(21, constants.size());
+        assertEquals(43, result.size());
+
+        var bookstoreTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[0]);
+        var bookstoreRenamedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[2]);
+        var bookstoreExtendedTokens = TokenUtils.tokenTypesByFile(result, TEST_SUBJECTS[1]);
+        assertTrue(bookstoreTokens.size() < bookstoreExtendedTokens.size());
+        assertIterableEquals(bookstoreTokens, bookstoreRenamedTokens);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        FileUtil.clearFiles(new File(BASE_PATH.toString()), Language.VIEW_FILE_SUFFIX);
+    }
+
+}

+ 16 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStore.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="owner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+        eType="#//Book" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="Book">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 38 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreExtended.ecore

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eSubpackages name="store" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/store"
+      nsPrefix="store">
+    <eClassifiers xsi:type="ecore:EClass" name="BookStore">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="location" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="books" upperBound="-1"
+          eType="#//store/Book" containment="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//person/Person"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Book">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isbn" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+          iD="true"/>
+      <eStructuralFeatures xsi:type="ecore:EReference" name="author" eType="#//person/Author"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="genre" eType="#//store/Genre"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EEnum" name="Genre">
+      <eLiterals name="NOVEL"/>
+      <eLiterals name="COOKBOOK" value="1"/>
+      <eLiterals name="BIOGRAPHY" value="3"/>
+      <eLiterals name="TEXTBOOK" value="4"/>
+    </eClassifiers>
+  </eSubpackages>
+  <eSubpackages name="person" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore/person"
+      nsPrefix="person">
+    <eClassifiers xsi:type="ecore:EClass" name="Author" eSuperTypes="#//person/Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="isStageName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/>
+    </eClassifiers>
+    <eClassifiers xsi:type="ecore:EClass" name="Person">
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="firstName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+      <eStructuralFeatures xsi:type="ecore:EAttribute" name="lastName" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    </eClassifiers>
+  </eSubpackages>
+</ecore:EPackage>

+ 16 - 0
jplag.frontend.emf-metamodel/src/test/resources/de/jplag/models/bookStoreRenamed.ecore

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ASCII"?>
+<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="BookStorePackage" nsURI="http:///com.ibm.dynamic.example.bookstore.ecore"
+    nsPrefix="bookStore">
+  <eClassifiers xsi:type="ecore:EClass" name="StoreThatSellsBooks">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="nameOfOwner" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="city" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EReference" name="soldItems" upperBound="-1"
+        eType="#//BookStoreItem" containment="true"/>
+  </eClassifiers>
+  <eClassifiers xsi:type="ecore:EClass" name="BookStoreItem">
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
+    <eStructuralFeatures xsi:type="ecore:EAttribute" name="identifier" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"
+        iD="true"/>
+  </eClassifiers>
+</ecore:EPackage>

+ 2 - 2
jplag.frontend.scala/pom.xml

@@ -30,7 +30,7 @@
         <dependency>
             <groupId>org.scalameta</groupId>
             <artifactId>scalameta_${scala.compat.version}</artifactId>
-            <version>4.5.9</version>
+            <version>4.5.11</version>
         </dependency>
 
         <!-- Test -->
@@ -49,7 +49,7 @@
                 <!-- see http://davidb.github.com/scala-maven-plugin -->
                 <groupId>net.alchim31.maven</groupId>
                 <artifactId>scala-maven-plugin</artifactId>
-                <version>3.3.2</version>
+                <version>4.7.1</version>
                 <executions>
                     <execution>
                         <goals>

+ 8 - 0
jplag/pom.xml

@@ -55,5 +55,13 @@
             <artifactId>kotlin</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>emf-metamodel-dynamic</artifactId>
+        </dependency>
     </dependencies>
 </project>

+ 1 - 0
jplag/src/main/java/de/jplag/SubmissionSetBuilder.java

@@ -75,6 +75,7 @@ public class SubmissionSetBuilder {
 
         // Merge everything in a submission set.
         List<Submission> submissions = new ArrayList<>(foundSubmissions.values());
+
         return new SubmissionSet(submissions, baseCodeSubmission.orElse(null), options);
     }
 

+ 10 - 3
jplag/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java

@@ -5,7 +5,11 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.text.SimpleDateFormat;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.slf4j.Logger;
@@ -13,6 +17,7 @@ import org.slf4j.LoggerFactory;
 
 import de.jplag.JPlagComparison;
 import de.jplag.JPlagResult;
+import de.jplag.Language;
 import de.jplag.Submission;
 import de.jplag.reporting.jsonfactory.FileWriter;
 import de.jplag.reporting.reportobject.mapper.ClusteringResultMapper;
@@ -90,13 +95,15 @@ public class ReportObjectFactory {
         List<JPlagComparison> comparisons = result.getComparisons(result.getOptions().getMaximumNumberOfComparisons());
         var submissions = getSubmissions(comparisons);
         var submissionsPath = createDirectory(path, SUBMISSIONS_FOLDER);
+        Language language = result.getOptions().getLanguage();
         for (var submission : submissions) {
             File directory = createDirectory(submissionsPath.getPath(), submission.getName());
             for (var file : submission.getFiles()) {
+                var fileToCopy = language.useViewFiles() ? new File(file.getPath() + language.viewFileSuffix()) : file;
                 try {
-                    Files.copy(file.toPath(), (new File(directory, file.getName())).toPath(), StandardCopyOption.REPLACE_EXISTING);
+                    Files.copy(fileToCopy.toPath(), (new File(directory, file.getName())).toPath(), StandardCopyOption.REPLACE_EXISTING);
                 } catch (IOException e) {
-                    logger.error("Could not save submission file " + file, e);
+                    logger.error("Could not save submission file " + fileToCopy, e);
                 }
             }
         }

+ 9 - 5
jplag/src/main/java/de/jplag/reporting/reportobject/mapper/ComparisonReportMapper.java

@@ -36,7 +36,7 @@ public class ComparisonReportMapper {
                 .map(match -> convertMatchToReportMatch(comparison, match, result.getOptions().getLanguage().supportsColumns())).toList();
     }
 
-    private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, boolean usesIndex) {
+    private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Match match, boolean languageSupportsColumnsAndLines) {
         TokenList tokensFirst = comparison.getFirstSubmission().getTokenList();
         TokenList tokensSecond = comparison.getSecondSubmission().getTokenList();
         Token startTokenFirst = tokensFirst.getToken(match.startOfFirst());
@@ -44,13 +44,17 @@ public class ComparisonReportMapper {
         Token startTokenSecond = tokensSecond.getToken(match.startOfSecond());
         Token endTokenSecond = tokensSecond.getToken(match.startOfSecond() + match.length() - 1);
 
-        int startFirst = usesIndex ? startTokenFirst.getIndex() : startTokenFirst.getLine();
-        int endFirst = usesIndex ? endTokenFirst.getIndex() : endTokenFirst.getLine();
-        int startSecond = usesIndex ? startTokenSecond.getIndex() : startTokenSecond.getLine();
-        int endSecond = usesIndex ? endTokenSecond.getIndex() : endTokenSecond.getLine();
+        int startFirst = getPosition(languageSupportsColumnsAndLines, startTokenFirst);
+        int endFirst = getPosition(languageSupportsColumnsAndLines, endTokenFirst);
+        int startSecond = getPosition(languageSupportsColumnsAndLines, startTokenSecond);
+        int endSecond = getPosition(languageSupportsColumnsAndLines, endTokenSecond);
         int tokens = match.length();
 
         return new Match(startTokenFirst.getFile(), startTokenSecond.getFile(), startFirst, endFirst, startSecond, endSecond, tokens);
     }
 
+    private int getPosition(boolean languageSupportsColumnsAndLines, Token token) {
+        return languageSupportsColumnsAndLines ? token.getLine() : token.getIndex();
+    }
+
 }

+ 16 - 0
jplag/src/test/java/de/jplag/NormalComparisonTest.java

@@ -1,5 +1,6 @@
 package de.jplag;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -27,6 +28,21 @@ class NormalComparisonTest extends TestBase {
         assertEquals(62.07f, result.getAllComparisons().get(0).similarity(), 0.1f);
     }
 
+    /**
+     * The simple duplicate with a custom min token match.
+     */
+    @Test
+    void testWithMinTokenMatch() throws ExitException {
+        var expectedDistribution = new int[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+        JPlagResult result = runJPlag("SimpleDuplicate", it -> it.setMinimumTokenMatch(5));
+
+        assertEquals(2, result.getNumberOfSubmissions());
+        assertEquals(1, result.getAllComparisons().size());
+        assertEquals(2, result.getAllComparisons().get(0).getMatches().size());
+        assertArrayEquals(expectedDistribution, result.getSimilarityDistribution());
+        assertEquals(96.55f, result.getAllComparisons().get(0).similarity(), 0.1f);
+    }
+
     /**
      * The classes in no duplicate have nearly nothing in common.
      */

+ 18 - 1
pom.xml

@@ -74,13 +74,16 @@
 
         <maven.compiler.source>17</maven.compiler.source>
         <maven.compiler.target>17</maven.compiler.target>
-        <spotless.version>2.24.0</spotless.version>
+        <spotless.version>2.24.1</spotless.version>
         <slf4j.version>1.7.36</slf4j.version>
         <junit.version>5.9.0</junit.version>
 
         <antlr2.version>2.7.7</antlr2.version>
         <antlr3.version>3.5</antlr3.version>
         <antlr4.version>4.10.1</antlr4.version>
+        <emf.version>2.27.0</emf.version>
+        <emf.ecore.version>2.25.0</emf.ecore.version>
+        <emf.ecore.xmi.version>2.16.0</emf.ecore.xmi.version>
 
         <!-- The Revision of JPlag -->
         <revision>4.0.0-SNAPSHOT</revision>
@@ -188,6 +191,16 @@
                 <artifactId>chars</artifactId>
                 <version>${revision}</version>
             </dependency>
+            <dependency>
+                <groupId>de.jplag</groupId>
+                <artifactId>emf-metamodel</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>de.jplag</groupId>
+                <artifactId>emf-metamodel-dynamic</artifactId>
+                <version>${revision}</version>
+            </dependency>
             <dependency>
                 <groupId>de.jplag</groupId>
                 <artifactId>frontend-utils</artifactId>
@@ -478,6 +491,8 @@
                 <module>jplag.frontend.scala</module>
                 <module>jplag.frontend.scheme</module>
                 <module>jplag.frontend.text</module>
+                <module>jplag.frontend.emf-metamodel</module>
+                <module>jplag.frontend.emf-metamodel-dynamic</module>
                 <module>jplag.endToEndTesting</module>
                 <module>jplag</module>
                 <module>jplag.cli</module>
@@ -502,6 +517,8 @@
                 <module>jplag.frontend.scala</module>
                 <module>jplag.frontend.scheme</module>
                 <module>jplag.frontend.text</module>
+                <module>jplag.frontend.emf-metamodel</module>
+                <module>jplag.frontend.emf-metamodel-dynamic</module>
                 <module>jplag</module>
             </modules>
             <build>

File diff suppressed because it is too large
+ 427 - 108
report-viewer/package-lock.json


+ 3 - 3
report-viewer/package.json

@@ -9,7 +9,7 @@
   },
   "dependencies": {
     "@highlightjs/vue-plugin": "^2.1.0",
-    "chart.js": "^3.9.0",
+    "chart.js": "^3.9.1",
     "chartjs-plugin-datalabels": "^2.1.0",
     "core-js": "^3.24.1",
     "gitart-vue-dialog": "^2.4.0",
@@ -24,8 +24,8 @@
     "vuex": "^4.0.2"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^5.32.0",
-    "@typescript-eslint/parser": "^5.32.0",
+    "@typescript-eslint/eslint-plugin": "^5.33.0",
+    "@typescript-eslint/parser": "^5.33.0",
     "@vue/cli-plugin-babel": "~5.0.8",
     "@vue/cli-plugin-eslint": "~5.0.8",
     "@vue/cli-plugin-router": "~5.0.8",

+ 30 - 21
report-viewer/src/components/CodePanel.vue

@@ -25,27 +25,32 @@
         />
       </button>
     </div>
-    <div :class="{ hidden: !collapse }" class="code-container">
-      <LineOfCode
-        v-for="(line, index) in lines"
-        :id="String(panelId).concat(title).concat(index)"
-        :key="index"
-        :color="coloringArray[index]"
-        :is-first="isFirst[index]"
-        :is-last="isLast[index]"
-        :line-number="index + 1"
-        :text="line"
-        :visible="collapse"
-        @click="
-          $emit(
-            'lineSelected',
-            $event,
-            linksArray[index].panel,
-            linksArray[index].file,
-            linksArray[index].line
-          )
-        "
-      />
+    <div :class="{ hidden: !collapse }">
+      <div v-if="!isEmpty(lines)" class="code-container">
+        <LineOfCode
+          v-for="(line, index) in lines"
+          :id="String(panelId).concat(title).concat(index)"
+          :key="index"
+          :color="coloringArray[index]"
+          :is-first="isFirst[index]"
+          :is-last="isLast[index]"
+          :line-number="index + 1"
+          :text="line"
+          :visible="collapse"
+          @click="
+            $emit(
+              'lineSelected',
+              $event,
+              linksArray[index].panel,
+              linksArray[index].file,
+              linksArray[index].line
+            )
+          "
+        />
+      </div>
+      <div v-else class="code-container">
+        <p>Empty File</p>
+      </div>
     </div>
   </div>
 </template>
@@ -112,6 +117,9 @@ export default defineComponent({
      * @type {Ref<UnwrapRef<{}>>}
      */
     const coloringArray = ref({});
+    const isEmpty = (lines) => {
+      return lines.length === 0 || lines.every((line) => !(line.trim()));
+    };
     /**
      * An object containing an object from which an id is to of the line to which this is linked is constructed.
      * Id object contains panel, file name, first line number of linked matched.
@@ -170,6 +178,7 @@ export default defineComponent({
       linksArray,
       isFirst,
       isLast,
+      isEmpty,
     };
   },
 });

+ 19 - 17
report-viewer/src/components/MatchTable.vue

@@ -13,20 +13,24 @@
         <th>File 2</th>
         <th>Tokens</th>
       </tr>
-      <tr v-for="( match, index ) in matches"
-          :key="String(index).concat(match.startInFirst).concat(match.startInSecond)"
-          :style="{ background : match.color }"
-          @click="$emit('matchSelected', $event, match)">
+      <tr
+        v-for="(match, index) in matches"
+        :key="
+          String(index).concat(match.startInFirst).concat(match.startInSecond)
+        "
+        :style="{ background: match.color }"
+        @click="$emit('matchSelected', $event, match)"
+      >
         <td>
           <div class="td-content">
             <p>{{ match.firstFile }}</p>
-            <p>({{ match.startInFirst }} - {{ match.endInFirst }})</p>
+            <p>({{ match.startInFirst + 1 }} - {{ match.endInFirst + 1 }})</p>
           </div>
         </td>
         <td>
           <div class="td-content">
             <p>{{ match.secondFile }}</p>
-            <p>({{ match.startInSecond }} - {{ match.endInSecond }})</p>
+            <p>({{ match.startInSecond + 1 }} - {{ match.endInSecond + 1 }})</p>
           </div>
         </td>
         <td>{{ match.tokens }}</td>
@@ -36,7 +40,7 @@
 </template>
 
 <script>
-import {defineComponent} from "vue";
+import { defineComponent } from "vue";
 
 export default defineComponent({
   name: "MatchTable",
@@ -46,25 +50,25 @@ export default defineComponent({
      * type: Array<Match>
      */
     matches: {
-      type: Array
+      type: Array,
     },
     /**
      * ID of first submission
      */
     id1: {
-      type: String
+      type: String,
     },
     /**
      * ID of second submission
      */
     id2: {
-      type: String
-    }
+      type: String,
+    },
   },
   setup() {
-    return {}
-  }
-})
+    return {};
+  },
+});
 </script>
 
 <style scoped>
@@ -97,13 +101,11 @@ table {
 th {
   color: var(--on-primary-color);
   text-align: center;
-
 }
 
 td {
   font-size: small;
   text-align: center;
   padding: 2%;
-
 }
-</style>
+</style>

+ 14 - 7
report-viewer/src/views/FileUploadView.vue

@@ -53,6 +53,13 @@ export default defineComponent({
         },
       });
     };
+    const extractSubmissionFileName = (filePath: path.ParsedPath) => {
+      const folders = filePath.dir.split("/");
+      const submissionFolderIndex = folders.findIndex(
+        (folder) => folder === "submissions"
+      );
+      return folders[submissionFolderIndex + 1];
+    };
     /**
      * Handles zip file on drop. It extracts the zip and saves each file in the store.
      * @param file
@@ -60,12 +67,15 @@ export default defineComponent({
     const handleZipFile = (file: File) => {
       jszip.loadAsync(file).then(async (zip) => {
         for (const fileName of Object.keys(zip.files)) {
-          if (fileName.match(/submissions\/(.+)\/(.+)/)) {
+          if (
+            /((.+\/)*)submissions\/(.+)\/(.+)/.test(fileName) &&
+            !/^__MACOSX\//.test(fileName)
+          ) {
             const filePath = path.parse(fileName);
-            const submissionName = filePath.dir.split("/")[2];
+            const submissionFileName = extractSubmissionFileName(filePath);
             await zip.files[fileName].async("string").then((data) => {
               store.commit("saveSubmissionFile", {
-                name: submissionName,
+                name: submissionFileName,
                 file: { fileName: filePath.base, data: data },
               });
             });
@@ -105,10 +115,7 @@ export default defineComponent({
           single: true,
           fileString: str,
         });
-        navigateToComparisonView(
-          json["id1"],
-          json["id2"]
-        );
+        navigateToComparisonView(json["id1"], json["id2"]);
       }
     };
     /**

Some files were not shown because too many files changed in this diff