Browse Source

Merge pull request #463 from jplag/feature/metainf

Dynamic Loading of Language Frontends & Fixed behavior of JavaC
Timur Sağlam 4 năm trước cách đây
mục cha
commit
d66365ca75
37 tập tin đã thay đổi với 342 bổ sung232 xóa
  1. 9 5
      jplag.cli/pom.xml
  2. 23 0
      jplag.cli/src/assembly/src.xml
  3. 1 3
      jplag.cli/src/main/java/de/jplag/CLI.java
  4. 11 4
      jplag.cli/src/main/java/de/jplag/CommandLineArgument.java
  5. 0 35
      jplag.cli/src/test/java/de/jplag/cli/LanguageOptionTest.java
  6. 43 0
      jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java
  7. 5 0
      jplag.endToEndTesting/pom.xml
  8. 6 7
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java
  9. 5 6
      jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java
  10. 2 2
      jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java
  11. 6 0
      jplag.frontend-utils/pom.xml
  12. 2 2
      jplag.frontend-utils/src/main/java/de/jplag/Language.java
  13. 75 0
      jplag.frontend-utils/src/main/java/de/jplag/LanguageLoader.java
  14. 7 2
      jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java
  15. 6 3
      jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java
  16. 6 3
      jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java
  17. 5 3
      jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java
  18. 6 3
      jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java
  19. 19 10
      jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java
  20. 7 2
      jplag.frontend.java/src/main/java/de/jplag/java/Language.java
  21. 6 3
      jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java
  22. 7 2
      jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java
  23. 6 3
      jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java
  24. 3 3
      jplag.frontend.rust/src/main/java/de/jplag/rust/Language.java
  25. 1 0
      jplag.frontend.scala/src/main/resources/META-INF/services/de.jplag.Language
  26. 3 1
      jplag.frontend.scala/src/main/scala/de/jplag/scala/Language.scala
  27. 7 2
      jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java
  28. 6 7
      jplag.frontend.text/src/main/java/de/jplag/text/Language.java
  29. 11 19
      jplag/pom.xml
  30. 16 20
      jplag/src/main/java/de/jplag/JPlag.java
  31. 1 1
      jplag/src/main/java/de/jplag/JPlagResult.java
  32. 1 1
      jplag/src/main/java/de/jplag/Submission.java
  33. 13 16
      jplag/src/main/java/de/jplag/options/JPlagOptions.java
  34. 0 53
      jplag/src/main/java/de/jplag/options/LanguageOption.java
  35. 2 1
      jplag/src/test/java/de/jplag/InvalidSubmissionTest.java
  36. 2 2
      jplag/src/test/java/de/jplag/TestBase.java
  37. 13 8
      jplag/src/test/java/de/jplag/special/TokenPrinterTest.java

+ 9 - 5
jplag.cli/pom.xml

@@ -12,6 +12,8 @@
             <groupId>de.jplag</groupId>
             <artifactId>jplag</artifactId>
         </dependency>
+
+        <!-- Languages -->
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
@@ -73,6 +75,7 @@
             <artifactId>endToEndTesting</artifactId>
         </dependency>
 
+        <!-- CLI -->
         <dependency>
             <groupId>org.kohsuke.metainf-services</groupId>
             <artifactId>metainf-services</artifactId>
@@ -96,17 +99,18 @@
                     </archive>
                 </configuration>
             </plugin>
-
             <plugin>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <configuration>
-                    <descriptorRefs>
-                        <descriptorRef>jar-with-dependencies</descriptorRef>
-                    </descriptorRefs>
+                <configuration combine.self="override">
+                    <descriptors>
+                        <descriptor>src/assembly/src.xml</descriptor>
+                    </descriptors>
                     <archive>
                         <manifest>
                             <mainClass>de.jplag.CLI</mainClass>
                             <packageName>jplag</packageName>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                         </manifest>
                     </archive>
                     <finalName>jplag-${project.version}</finalName>

+ 23 - 0
jplag.cli/src/assembly/src.xml

@@ -0,0 +1,23 @@
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.1"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.1 https://maven.apache.org/xsd/assembly-2.1.1.xsd">
+    <id>jar-with-dependencies</id>
+    <formats>
+        <format>jar</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <dependencySets>
+        <dependencySet>
+            <outputDirectory>/</outputDirectory>
+            <useProjectArtifact>true</useProjectArtifact>
+            <unpack>true</unpack>
+            <scope>runtime</scope>
+        </dependencySet>
+    </dependencySets>
+    <!-- Merge MetaInf-Services in final JAR -->
+    <containerDescriptorHandlers>
+        <containerDescriptorHandler>
+            <handlerName>metaInf-services</handlerName>
+        </containerDescriptorHandler>
+    </containerDescriptorHandlers>
+</assembly>

+ 1 - 3
jplag.cli/src/main/java/de/jplag/CLI.java

@@ -24,7 +24,6 @@ import de.jplag.clustering.algorithm.InterClusterSimilarity;
 import de.jplag.exceptions.ExitException;
 import de.jplag.logger.CollectedLoggerFactory;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.options.Verbosity;
 import de.jplag.reporting.reportobject.ReportObjectFactory;
@@ -126,8 +125,7 @@ public final class CLI {
         addAllMultiValueArgument(NEW_DIRECTORY.getListFrom(namespace), submissionDirectories);
         addAllMultiValueArgument(OLD_DIRECTORY.getListFrom(namespace), oldSubmissionDirectories);
 
-        LanguageOption language = LanguageOption.fromDisplayName(LANGUAGE.getFrom(namespace));
-        JPlagOptions options = new JPlagOptions(submissionDirectories, oldSubmissionDirectories, language);
+        JPlagOptions options = new JPlagOptions(submissionDirectories, oldSubmissionDirectories, LANGUAGE.getFrom(namespace));
         options.setBaseCodeSubmissionName(BASE_CODE.getFrom(namespace));
         options.setVerbosity(Verbosity.fromOption(VERBOSITY.getFrom(namespace)));
         options.setDebugParser(DEBUG.getFrom(namespace));

+ 11 - 4
jplag.cli/src/main/java/de/jplag/CommandLineArgument.java

@@ -24,7 +24,6 @@ import net.sourceforge.argparse4j.inf.Namespace;
 import de.jplag.clustering.ClusteringAlgorithm;
 import de.jplag.clustering.ClusteringOptions;
 import de.jplag.clustering.algorithm.InterClusterSimilarity;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.SimilarityMetric;
 import de.jplag.strategy.ComparisonMode;
 
@@ -36,7 +35,9 @@ public enum CommandLineArgument {
     ROOT_DIRECTORY(new Builder("rootDir", String.class).nargs(NumberOfArgumentValues.ZERO_OR_MORE_VALUES)),
     NEW_DIRECTORY(new Builder("-new", String.class).nargs(NumberOfArgumentValues.ONE_OR_MORE_VALUES)),
     OLD_DIRECTORY(new Builder("-old", String.class).nargs(NumberOfArgumentValues.ONE_OR_MORE_VALUES)),
-    LANGUAGE(new Builder("-l", String.class).defaultsTo(LanguageOption.getDefault().getDisplayName()).choices(LanguageOption.getAllDisplayNames())),
+    LANGUAGE(
+            new Builder("-l", String.class).defaultsTo(de.jplag.java.Language.IDENTIFIER)
+                    .choices(LanguageLoader.getAllAvailableLanguageIdentifiers())),
     BASE_CODE("-bc", String.class),
     VERBOSITY(new Builder("-v", String.class).defaultsTo("quiet").choices("quiet", "long").argumentGroup(ADVANCED_GROUP)), // TODO SH: Replace
                                                                                                                            // verbosity when
@@ -84,6 +85,12 @@ public enum CommandLineArgument {
     CLUSTER_PREPROCESSING_PERCENTILE(new Builder("--cluster-pp-percentile", Float.class).metaVar("percentile").hidden()),
     CLUSTER_PREPROCESSING_THRESHOLD(new Builder("--cluster-pp-threshold", Float.class).metaVar("threshold").hidden());
 
+    /**
+     * The identifier of the default {@link Language}.
+     * @see Language#getIdentifier()
+     */
+    public static final String DEFAULT_LANGUAGE_IDENTIFIER = de.jplag.java.Language.IDENTIFIER;
+
     private final String flag;
     private final NumberOfArgumentValues numberOfValues;
     private final String description;
@@ -96,11 +103,11 @@ public enum CommandLineArgument {
     private final Class<?> type;
     private final boolean hidden;
 
-    private CommandLineArgument(String flag, Class<?> type) {
+    CommandLineArgument(String flag, Class<?> type) {
         this(new Builder(flag, type));
     }
 
-    private CommandLineArgument(Builder builder) {
+    CommandLineArgument(Builder builder) {
         this.flag = builder.flag;
         this.type = builder.type;
         this.defaultValue = builder.defaultValue;

+ 0 - 35
jplag.cli/src/test/java/de/jplag/cli/LanguageOptionTest.java

@@ -1,35 +0,0 @@
-package de.jplag.cli;
-
-import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-
-import de.jplag.CommandLineArgument;
-import de.jplag.options.LanguageOption;
-
-public class LanguageOptionTest extends CommandLineInterfaceTest {
-
-    @Test
-    public void testDefaultLanguage() {
-        buildOptionsFromCLI(CURRENT_DIRECTORY);
-        assertEquals(LanguageOption.getDefault(), options.getLanguageOption());
-    }
-
-    @Test
-    public void testInvalidLanguage() throws Exception {
-        String argument = buildArgument(CommandLineArgument.LANGUAGE, "Piet");
-        int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
-        assertEquals(1, statusCode);
-    }
-
-    @Test
-    public void testValidLanguages() {
-        for (LanguageOption language : LanguageOption.values()) {
-            String argument = buildArgument(CommandLineArgument.LANGUAGE, language.getDisplayName());
-            buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
-            assertEquals(language, options.getLanguageOption());
-        }
-    }
-
-}

+ 43 - 0
jplag.cli/src/test/java/de/jplag/cli/LanguageTest.java

@@ -0,0 +1,43 @@
+package de.jplag.cli;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import de.jplag.CommandLineArgument;
+import de.jplag.Language;
+import de.jplag.LanguageLoader;
+
+class LanguageTest extends CommandLineInterfaceTest {
+
+    @Test
+    void testDefaultLanguage() {
+        buildOptionsFromCLI(CURRENT_DIRECTORY);
+        assertEquals(CommandLineArgument.DEFAULT_LANGUAGE_IDENTIFIER, options.getLanguageIdentifier());
+    }
+
+    @Test
+    void testInvalidLanguage() throws Exception {
+        String argument = buildArgument(CommandLineArgument.LANGUAGE, "Piet");
+        int statusCode = catchSystemExit(() -> buildOptionsFromCLI(argument, CURRENT_DIRECTORY));
+        assertEquals(1, statusCode);
+    }
+
+    @Test
+    void testLoading() {
+        var languages = LanguageLoader.getAllAvailableLanguages();
+        Assertions.assertEquals(12, languages.size(), "Loaded Languages: " + languages.keySet());
+    }
+
+    @Test
+    void testValidLanguages() {
+        for (Language language : LanguageLoader.getAllAvailableLanguages().values()) {
+            String argument = buildArgument(CommandLineArgument.LANGUAGE, language.getIdentifier());
+            buildOptionsFromCLI(argument, CURRENT_DIRECTORY);
+            assertEquals(language.getIdentifier(), options.getLanguageIdentifier());
+        }
+    }
+
+}

+ 5 - 0
jplag.endToEndTesting/pom.xml

@@ -13,6 +13,11 @@
             <groupId>de.jplag</groupId>
             <artifactId>jplag</artifactId>
         </dependency>
+        <dependency>
+            <groupId>de.jplag</groupId>
+            <artifactId>java</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-testutils</artifactId>

+ 6 - 7
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/helper/JPlagTestSuiteHelper.java

@@ -15,7 +15,6 @@ import org.slf4j.LoggerFactory;
 import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
 import de.jplag.end_to_end_testing.model.JsonModel;
 import de.jplag.end_to_end_testing.model.TestCaseModel;
-import de.jplag.options.LanguageOption;
 
 /**
  * This helper class deals with creating the test cases as well as copying and deleting for the end-to-end tests. The
@@ -28,18 +27,18 @@ public class JPlagTestSuiteHelper {
 
     private String[] resourceNames;
     private List<JsonModel> resultModel;
-    private LanguageOption languageOption;
+    private String languageIdentifier;
 
     /**
      * Helper class for the endToEnd tests. In this class the necessary resources are loaded, prepared and copied for the
      * tests based on the passed parameters. An instance of this class loads all necessary paths and properties for a test
      * run with the specified language
-     * @param languageOption for loading language-specific resources
+     * @param languageIdentifier for loading language-specific resources
      * @throws IOException is thrown for all problems that may occur while parsing the json file. This includes both reading
      * and parsing problems.
      */
-    public JPlagTestSuiteHelper(LanguageOption languageOption) throws IOException {
-        this.languageOption = languageOption;
+    public JPlagTestSuiteHelper(String languageIdentifier) throws IOException {
+        this.languageIdentifier = languageIdentifier;
         this.resourceNames = new File(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toString()).list();
 
         this.resultModel = JsonHelper.getResultModelFromPath();
@@ -56,7 +55,7 @@ public class JPlagTestSuiteHelper {
     public TestCaseModel createNewTestCase(String[] classNames, String functionName) throws IOException {
         createNewTestCaseDirectory(classNames);
         JsonModel resultJsonModel = resultModel.stream().filter(jsonModel -> functionName.equals(jsonModel.getFunctionName())).findAny().orElse(null);
-        return new TestCaseModel(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME, resultJsonModel, languageOption);
+        return new TestCaseModel(TestDirectoryConstants.TEMPORARY_SUBMISSION_DIRECTORY_NAME, resultJsonModel, languageIdentifier);
     }
 
     /**
@@ -98,7 +97,7 @@ public class JPlagTestSuiteHelper {
 
     /**
      * Delete directory with including files
-     * @param file Path to a folder or file to be deleted. This happens recursively to the path
+     * @param folder Path to a folder or file to be deleted. This happens recursively to the path
      * @throws IOException if an I/O error occurs
      */
     private void deleteCopiedFiles(File folder) throws IOException {

+ 5 - 6
jplag.endToEndTesting/src/main/java/de/jplag/end_to_end_testing/model/TestCaseModel.java

@@ -4,7 +4,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 
 /**
  * Median class for trading the results and the paths used in the tests. Also the JPlagOptions used for a test run are
@@ -13,19 +12,19 @@ import de.jplag.options.LanguageOption;
 public class TestCaseModel {
 
     private JsonModel jsonModel;
-    private LanguageOption languageOption;
+    private String languageIdentifier;
     private String submissionFolderPath;
 
     /**
      * Constructor for creating the instance
      * @param submissionFolderPath path to the plagiarism folder structure to be tested using Jplag
      * @param jsonModel the parsed result model needed for the test to be able to match it later in the tests.
-     * @param languageOption the language selection in which the tests take place
+     * @param languageIdentifier the language selection in which the tests take place
      */
-    public TestCaseModel(String submissionFolderPath, JsonModel jsonModel, LanguageOption languageOption) {
+    public TestCaseModel(String submissionFolderPath, JsonModel jsonModel, String languageIdentifier) {
         this.submissionFolderPath = submissionFolderPath;
         this.jsonModel = jsonModel;
-        this.languageOption = languageOption;
+        this.languageIdentifier = languageIdentifier;
     }
 
     /**
@@ -41,6 +40,6 @@ public class TestCaseModel {
      * @return current JPlag options for the created object
      */
     public JPlagOptions getJPlagOptionsFromCurrentModel() {
-        return new JPlagOptions(new ArrayList<>(Arrays.asList(submissionFolderPath)), new ArrayList<>(), languageOption);
+        return new JPlagOptions(new ArrayList<>(Arrays.asList(submissionFolderPath)), new ArrayList<>(), languageIdentifier);
     }
 }

+ 2 - 2
jplag.endToEndTesting/src/test/java/de/jplag/end_to_end_testing/JavaEndToEndTest.java

@@ -20,7 +20,7 @@ import de.jplag.end_to_end_testing.constants.TestDirectoryConstants;
 import de.jplag.end_to_end_testing.helper.JPlagTestSuiteHelper;
 import de.jplag.end_to_end_testing.model.TestCaseModel;
 import de.jplag.exceptions.ExitException;
-import de.jplag.options.LanguageOption;
+import de.jplag.java.Language;
 
 /**
  * Main test class for end-to-end testing in the Java language. The test cases aim to detect changes in the detection of
@@ -34,7 +34,7 @@ class JavaEndToEndTest {
 
     @BeforeAll
     public void setUp() throws IOException {
-        jplagTestSuiteHelper = new JPlagTestSuiteHelper(LanguageOption.JAVA);
+        jplagTestSuiteHelper = new JPlagTestSuiteHelper(Language.IDENTIFIER);
         assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESOURCES_SORTALGO.toFile().exists(), "Could not find base directory!");
         assertTrue(TestDirectoryConstants.BASE_PATH_TO_JAVA_RESULT_JSON.toFile().isFile(), "Could not find java result json!");
     }

+ 6 - 0
jplag.frontend-utils/pom.xml

@@ -8,4 +8,10 @@
         <version>${revision}</version>
     </parent>
     <artifactId>frontend-utils</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.kohsuke.metainf-services</groupId>
+            <artifactId>metainf-services</artifactId>
+        </dependency>
+    </dependencies>
 </project>

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

@@ -18,9 +18,9 @@ public interface Language {
     String getName();
 
     /**
-     * Short name of the language used for CLI options.
+     * Identifier of the language used for CLI options and dynamic loading. You should use some name within {@code [a-z_-]+}
      */
-    String getShortName();
+    String getIdentifier();
 
     /**
      * Minimum number of tokens required for a match.

+ 75 - 0
jplag.frontend-utils/src/main/java/de/jplag/LanguageLoader.java

@@ -0,0 +1,75 @@
+package de.jplag;
+
+import java.util.*;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class contains methods to load {@link Language Languages}.
+ * @author Dominik Fuchss
+ */
+public final class LanguageLoader {
+    private static final Logger logger = LoggerFactory.getLogger(LanguageLoader.class);
+
+    private static Map<String, Language> cachedLanguageInstances = null;
+
+    private LanguageLoader() {
+        throw new IllegalAccessError();
+    }
+
+    /**
+     * Get all languages that are currently in the classpath. The languages will be cached. Use {@link #clearCache()} to
+     * obtain new instances.
+     * @return the languages as unmodifiable map from identifier to language instance.
+     */
+    public static synchronized Map<String, Language> getAllAvailableLanguages() {
+        if (cachedLanguageInstances != null)
+            return cachedLanguageInstances;
+
+        Map<String, Language> languages = new TreeMap<>();
+
+        for (Language language : ServiceLoader.load(Language.class)) {
+            String languageIdentifier = language.getIdentifier();
+            if (languages.containsKey(languageIdentifier)) {
+                logger.error("Multiple implementations for a language '{}' are present in the classpath! Skipping ..", languageIdentifier);
+                languages.remove(languageIdentifier);
+                continue;
+            }
+            logger.info("Loading Language Frontend '{}'", language.getName());
+            languages.put(languageIdentifier, language);
+        }
+
+        cachedLanguageInstances = Collections.unmodifiableMap(languages);
+        return cachedLanguageInstances;
+    }
+
+    /**
+     * Load a language that is currently in the classpath by its short name.
+     * @param identifier the identifier of the language
+     * @return the language or an empty optional if no language has been found.
+     * @see Language#getIdentifier()
+     */
+    public static Optional<Language> getLanguage(String identifier) {
+        var language = getAllAvailableLanguages().get(identifier);
+        if (language == null)
+            logger.warn("Attempt to load Language {} was not successful", identifier);
+        return Optional.ofNullable(language);
+    }
+
+    /**
+     * Get an unmodifiable set of all available languages with their identifiers.
+     * @return identifiers of all available languages
+     * @see Language#getIdentifier()
+     */
+    public static Set<String> getAllAvailableLanguageIdentifiers() {
+        return new TreeSet<>(getAllAvailableLanguages().keySet());
+    }
+
+    /**
+     * Resets the internal cache of all loaded languages
+     */
+    public static synchronized void clearCache() {
+        cachedLanguageInstances = null;
+    }
+}

+ 7 - 2
jplag.frontend.cpp/src/main/java/de/jplag/cpp/Language.java

@@ -2,9 +2,14 @@ package de.jplag.cpp;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+    public static final String IDENTIFIER = "cpp";
+
     private final Scanner scanner; // cpp code is scanned not parsed
 
     public Language() {
@@ -22,8 +27,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "cpp";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.csharp-6/src/main/java/de/jplag/csharp/Language.java

@@ -3,15 +3,18 @@ package de.jplag.csharp;
 import java.io.File;
 import java.util.Arrays;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * C# language with full support of C# 6 features and below.
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
     private static final String NAME = "C# 6 Parser";
-    private static final String SHORT_NAME = "C# 6";
+    private static final String IDENTIFIER = "csharp";
     private static final String[] FILE_ENDINGS = new String[] {".cs", ".CS"};
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
 
@@ -32,8 +35,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.emf-metamodel-dynamic/src/main/java/de/jplag/emf/dynamic/Language.java

@@ -1,5 +1,7 @@
 package de.jplag.emf.dynamic;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
 
 /**
@@ -7,9 +9,10 @@ import de.jplag.emf.dynamic.parser.DynamicEcoreParser;
  * created token set instead of a hand-picked one.
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 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)";
+    public static final String IDENTIFIER = "emf-metamodel-dynamic";
 
     private static final int DEFAULT_MIN_TOKEN_MATCH = 10;
 
@@ -23,8 +26,8 @@ public class Language extends de.jplag.emf.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 5 - 3
jplag.frontend.emf-metamodel/src/main/java/de/jplag/emf/Language.java

@@ -4,6 +4,7 @@ import java.io.File;
 import java.util.Arrays;
 
 import org.eclipse.emf.ecore.EcorePackage;
+import org.kohsuke.MetaInfServices;
 
 import de.jplag.TokenList;
 import de.jplag.emf.parser.EcoreParser;
@@ -12,12 +13,13 @@ import de.jplag.emf.parser.EcoreParser;
  * Language for EMF metamodels from the Eclipse Modeling Framework (EMF).
  * @author Timur Saglam
  */
+@MetaInfServices(de.jplag.Language.class)
 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";
+    public static final String IDENTIFIER = "emf-metamodel";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 6;
 
     protected final EcoreParser parser;
@@ -41,8 +43,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.golang/src/main/java/de/jplag/golang/Language.java

@@ -2,12 +2,15 @@ package de.jplag.golang;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "Go Parser";
-    private static final String SHORT_NAME = "Go";
+    public static final String IDENTIFIER = "go";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSIONS = {".go"};
     private final GoParserAdapter parserAdapter;
@@ -27,8 +30,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 19 - 10
jplag.frontend.java/src/main/java/de/jplag/java/JavacAdapter.java

@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.List;
 
 import javax.tools.*;
 import javax.tools.JavaCompiler.CompilationTask;
@@ -24,17 +25,25 @@ public class JavacAdapter {
     private static final JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
 
     public int parseFiles(File directory, Iterable<File> pathedFiles, final Parser parser) {
-        final StandardJavaFileManager fileManager = javac.getStandardFileManager(null, null, StandardCharsets.UTF_8);
         var listener = new DiagnosticCollector<>();
-        var javaFiles = fileManager.getJavaFileObjectsFromFiles(pathedFiles);
-        final CompilationTask task = javac.getTask(null, fileManager, listener, null, null, javaFiles);
-        final Trees trees = Trees.instance(task);
-        final SourcePositions positions = trees.getSourcePositions();
-        for (final CompilationUnitTree ast : executeCompilationTask(task)) {
-            final String filename = fileNameOf(directory, ast);
-            final LineMap map = ast.getLineMap();
-            ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
-            parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
+
+        try (final StandardJavaFileManager fileManager = javac.getStandardFileManager(listener, null, StandardCharsets.UTF_8)) {
+            var javaFiles = fileManager.getJavaFileObjectsFromFiles(pathedFiles);
+
+            // We need to disable annotation processing
+            // See
+            // https://stackoverflow.com/questions/72737445/system-java-compiler-behaves-different-depending-on-dependencies-defined-in-mave
+            final CompilationTask task = javac.getTask(null, fileManager, listener, List.of("-proc:none"), null, javaFiles);
+            final Trees trees = Trees.instance(task);
+            final SourcePositions positions = trees.getSourcePositions();
+            for (final CompilationUnitTree ast : executeCompilationTask(task)) {
+                final String filename = fileNameOf(directory, ast);
+                final LineMap map = ast.getLineMap();
+                ast.accept(new TokenGeneratingTreeScanner(filename, parser, map, positions, ast), null);
+                parser.add(TokenConstants.FILE_END, filename, 1, -1, -1);
+            }
+        } catch (IOException e) {
+            parser.logger.error(e.getMessage(), e);
         }
         return processErrors(parser.logger, listener);
     }

+ 7 - 2
jplag.frontend.java/src/main/java/de/jplag/java/Language.java

@@ -2,12 +2,17 @@ package de.jplag.java;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * Language for Java 9 and newer.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+    public static final String IDENTIFIER = "java";
+
     private final Parser parser;
 
     public Language() {
@@ -25,8 +30,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "java";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.kotlin/src/main/java/de/jplag/kotlin/Language.java

@@ -2,15 +2,18 @@ package de.jplag.kotlin;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * This represents the Kotlin language as a language supported by JPlag.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "Kotlin Parser";
-    private static final String SHORT_NAME = "Kotlin";
+    public static final String IDENTIFIER = "kotlin";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSIONS = {".kt"};
     private final KotlinParserAdapter parserAdapter;
@@ -30,8 +33,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 7 - 2
jplag.frontend.python-3/src/main/java/de/jplag/python3/Language.java

@@ -2,10 +2,15 @@ package de.jplag.python3;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
+    public static final String IDENTIFIER = "python3";
+
     private final Parser parser;
 
     public Language() {
@@ -23,8 +28,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "python3";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 3
jplag.frontend.rlang/src/main/java/de/jplag/rlang/Language.java

@@ -2,15 +2,18 @@ package de.jplag.rlang;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
  * This represents the R language as a language supported by JPlag.
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
     private static final String NAME = "R Parser";
-    private static final String SHORT_NAME = "R";
+    public static final String IDENTIFIER = "rlang";
     private static final int DEFAULT_MIN_TOKEN_MATCH = 8;
     private static final String[] FILE_EXTENSION = {".R", ".r"};
     private final RParserAdapter parserAdapter;
@@ -30,8 +33,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 3 - 3
jplag.frontend.rust/src/main/java/de/jplag/rust/Language.java

@@ -8,7 +8,7 @@ public class Language implements de.jplag.Language {
 
     protected static final String[] FILE_EXTENSIONS = {".rs"};
     public static final String NAME = "Rust frontend";
-    public static final String SHORT_NAME = "Rust";
+    public static final String IDENTIFIER = "rust";
     public static final int MINIMUM_TOKEN_MATCH = 8;
 
     private final RustParserAdapter parserAdapter;
@@ -28,8 +28,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return SHORT_NAME;
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 1 - 0
jplag.frontend.scala/src/main/resources/META-INF/services/de.jplag.Language

@@ -0,0 +1 @@
+de.jplag.scala.Language

+ 3 - 1
jplag.frontend.scala/src/main/scala/de/jplag/scala/Language.scala

@@ -4,6 +4,8 @@ import de.jplag.TokenList
 
 import java.io.File
 
+import org.kohsuke.MetaInfServices
+
 class Language extends de.jplag.Language {
   private val parser = new Parser
   private final val fileExtensions = Array(".scala", ".sc")
@@ -12,7 +14,7 @@ class Language extends de.jplag.Language {
 
   override def getName = "Scala parser"
 
-  override def getShortName = "scala"
+  override def getIdentifier = "scala"
 
   override def minimumTokenMatch = 8
 

+ 7 - 2
jplag.frontend.scheme/src/main/java/de/jplag/scheme/Language.java

@@ -2,9 +2,14 @@ package de.jplag.scheme;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
+
+    public static final String IDENTIFIER = "scheme";
     private final de.jplag.scheme.Parser parser;
 
     public Language() {
@@ -22,8 +27,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "scheme";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override

+ 6 - 7
jplag.frontend.text/src/main/java/de/jplag/text/Language.java

@@ -2,6 +2,8 @@ package de.jplag.text;
 
 import java.io.File;
 
+import org.kohsuke.MetaInfServices;
+
 import de.jplag.TokenList;
 
 /**
@@ -9,8 +11,10 @@ import de.jplag.TokenList;
  * individual words are interpreted as token types. Whitespace and special characters are ignored. This approach works,
  * but there are better approaches for text plagiarism out there (based on NLP techniques).
  */
+@MetaInfServices(de.jplag.Language.class)
 public class Language implements de.jplag.Language {
 
+    public static final String IDENTIFIER = "text";
     private final ParserAdapter parserAdapter;
 
     public Language() {
@@ -28,8 +32,8 @@ public class Language implements de.jplag.Language {
     }
 
     @Override
-    public String getShortName() {
-        return "text";
+    public String getIdentifier() {
+        return IDENTIFIER;
     }
 
     @Override
@@ -46,9 +50,4 @@ public class Language implements de.jplag.Language {
     public boolean hasErrors() {
         return parserAdapter.hasErrors();
     }
-
-    @Override
-    public boolean isPreformatted() {
-        return false;
-    }
 }

+ 11 - 19
jplag/pom.xml

@@ -25,53 +25,45 @@
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
         </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>text</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>java</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>python-3</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>csharp-6</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>cpp</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>golang</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>de.jplag</groupId>
-            <artifactId>kotlin</artifactId>
+            <artifactId>rlang</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>rlang</artifactId>
+            <artifactId>golang</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>scala</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
-            <artifactId>scheme</artifactId>
+            <artifactId>kotlin</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>emf-metamodel</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>emf-metamodel-dynamic</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 </project>

+ 16 - 20
jplag/src/main/java/de/jplag/JPlag.java

@@ -5,8 +5,6 @@ import static de.jplag.options.Verbosity.LONG;
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
 import java.util.Collections;
 import java.util.Optional;
 import java.util.Set;
@@ -19,7 +17,6 @@ import de.jplag.clustering.ClusteringFactory;
 import de.jplag.exceptions.ExitException;
 import de.jplag.exceptions.SubmissionException;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.strategy.ComparisonMode;
 import de.jplag.strategy.ComparisonStrategy;
 import de.jplag.strategy.NormalComparisonStrategy;
@@ -45,7 +42,7 @@ public class JPlag {
     public JPlag(JPlagOptions options) {
         this.options = options;
         coreAlgorithm = new GreedyStringTiling(options);
-        language = initializeLanguage();
+        language = initializeLanguage(this.options);
         comparisonStrategy = initializeComparisonStrategy(options.getComparisonMode());
         excludedFileNames = Optional.ofNullable(this.options.getExclusionFileName()).map(this::readExclusionFile).orElse(Collections.emptySet());
         options.setExcludedFiles(excludedFileNames); // store for report
@@ -92,7 +89,8 @@ public class JPlag {
 
         // Compare valid submissions.
         JPlagResult result = comparisonStrategy.compareSubmissions(submissionSet);
-        logger.info("\nTotal time for comparing submissions: " + TimeUtil.formatDuration(result.getDuration()));
+        if (logger.isInfoEnabled())
+            logger.info("Total time for comparing submissions: {}", TimeUtil.formatDuration(result.getDuration()));
 
         result.setClusteringResult(ClusteringFactory.getClusterings(result.getAllComparisons(), options.getClusteringOptions()));
 
@@ -106,22 +104,20 @@ public class JPlag {
         };
     }
 
-    private Language initializeLanguage() {
-        LanguageOption languageOption = this.options.getLanguageOption();
-
-        try {
-            Constructor<?> constructor = Class.forName(languageOption.getClassPath()).getConstructor();
-            Language language = (Language) constructor.newInstance();
-
-            this.options.setLanguage(language);
-            this.options.setLanguageDefaults(language);
-            logger.info("Initialized language {}", language.getName());
-            return language;
-        } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException
-                | IllegalArgumentException | InvocationTargetException e) {
-            logger.error(e.getMessage(), e);
-            throw new IllegalStateException("Language instantiation failed:" + e.getMessage(), e);
+    private static Language initializeLanguage(JPlagOptions options) {
+        String languageIdentifier = options.getLanguageIdentifier();
+        Language currentLanguage = options.getLanguage();
+
+        if (currentLanguage != null && (languageIdentifier == null || languageIdentifier.equals(currentLanguage.getIdentifier()))) {
+            // Ensure that we do not rely on the ServiceLoader API. We can also load an arbitrary language via Options
+            options.setLanguageDefaults(currentLanguage);
+            return currentLanguage;
         }
 
+        Language language = LanguageLoader.getLanguage(languageIdentifier).orElseThrow();
+        options.setLanguage(language);
+        options.setLanguageDefaults(language);
+        logger.info("Loaded language {}", language.getName());
+        return language;
     }
 }

+ 1 - 1
jplag/src/main/java/de/jplag/JPlagResult.java

@@ -124,7 +124,7 @@ public class JPlagResult {
     @Override
     public String toString() {
         return String.format("JPlagResult { comparisons: %d, duration: %d ms, language: %s, submissions: %d }", getAllComparisons().size(),
-                getDuration(), getOptions().getLanguageOption(), submissions.numberOfSubmissions());
+                getDuration(), getOptions().getLanguage().getName(), submissions.numberOfSubmissions());
     }
 
     /**

+ 1 - 1
jplag/src/main/java/de/jplag/Submission.java

@@ -218,7 +218,7 @@ public class Submission implements Comparable<Submission> {
     private void copySubmission() {
         File rootDirectory = submissionRootFile.getParentFile();
         assert rootDirectory != null;
-        File submissionDirectory = createSubdirectory(rootDirectory, ERROR_FOLDER, language.getShortName(), name);
+        File submissionDirectory = createSubdirectory(rootDirectory, ERROR_FOLDER, language.getIdentifier(), name);
         for (File file : files) {
             try {
                 Files.copy(file.toPath(), new File(submissionDirectory, file.getName()).toPath());

+ 13 - 16
jplag/src/main/java/de/jplag/options/JPlagOptions.java

@@ -20,13 +20,17 @@ import de.jplag.strategy.ComparisonMode;
 public class JPlagOptions {
 
     private static final Logger logger = LoggerFactory.getLogger("JPlag");
-
     public static final ComparisonMode DEFAULT_COMPARISON_MODE = NORMAL;
     public static final float DEFAULT_SIMILARITY_THRESHOLD = 0;
     public static final int DEFAULT_SHOWN_COMPARISONS = 30;
 
     public static final Charset CHARSET = StandardCharsets.UTF_8;
 
+    /**
+     * The identifier of the language used to parse the submissions.
+     */
+    private final String languageIdentifier;
+
     /**
      * Language used to parse the submissions.
      */
@@ -116,11 +120,6 @@ public class JPlagOptions {
      */
     private String subdirectoryName;
 
-    /**
-     * Language to use when parsing the submissions.
-     */
-    private LanguageOption languageOption;
-
     /**
      * Level of output verbosity.
      */
@@ -133,11 +132,13 @@ public class JPlagOptions {
 
     /**
      * Constructor with required attributes.
+     * @param languageIdentifier the identifier of the language to use. If set to {@code null} you have to use
+     * {@link #setLanguage(Language)} to set the language programmatically.
      */
-    public JPlagOptions(List<String> submissionDirectories, List<String> oldSubmissionDirectories, LanguageOption languageOption) {
+    public JPlagOptions(List<String> submissionDirectories, List<String> oldSubmissionDirectories, String languageIdentifier) {
         this.submissionDirectories = submissionDirectories;
         this.oldSubmissionDirectories = oldSubmissionDirectories;
-        this.languageOption = languageOption;
+        this.languageIdentifier = languageIdentifier;
     }
 
     public Optional<String> getBaseCodeSubmissionName() {
@@ -160,12 +161,12 @@ public class JPlagOptions {
         return fileSuffixes;
     }
 
-    public Language getLanguage() {
-        return language;
+    public String getLanguageIdentifier() {
+        return languageIdentifier;
     }
 
-    public LanguageOption getLanguageOption() {
-        return languageOption;
+    public Language getLanguage() {
+        return language;
     }
 
     public int getMaximumNumberOfComparisons() {
@@ -259,10 +260,6 @@ public class JPlagOptions {
         }
     }
 
-    public void setLanguageOption(LanguageOption languageOption) {
-        this.languageOption = languageOption;
-    }
-
     public void setMaximumNumberOfComparisons(int maximumNumberOfComparisons) {
         this.maximumNumberOfComparisons = Math.max(maximumNumberOfComparisons, -1);
     }

+ 0 - 53
jplag/src/main/java/de/jplag/options/LanguageOption.java

@@ -1,53 +0,0 @@
-package de.jplag.options;
-
-import java.util.Arrays;
-import java.util.Collection;
-
-/**
- * The available languages.
- */
-public enum LanguageOption {
-    JAVA("java", "de.jplag.java.Language"),
-    PYTHON_3("python3", "de.jplag.python3.Language"),
-    C_CPP("cpp", "de.jplag.cpp.Language"),
-    C_SHARP("csharp", "de.jplag.csharp.Language"),
-    GO_LANG("golang", "de.jplag.golang.Language"),
-    KOTLIN("kotlin", "de.jplag.kotlin.Language"),
-    R_LANG("rlang", "de.jplag.rlang.Language"),
-    RUST("rust", "de.jplag.rust.Language"),
-    SCALA("scala", "de.jplag.scala.Language"),
-    CHAR("char", "de.jplag.chars.Language"),
-    TEXT("text", "de.jplag.text.Language"),
-    SCHEME("scheme", "de.jplag.scheme.Language"),
-    EMF("emf-metamodel", "de.jplag.emf.Language"),
-    EMF_DYNAMIC("emf-metamodel-dynamic", "de.jplag.emf.dynamic.Language");
-
-    private final String classPath;
-    private final String displayName;
-
-    LanguageOption(String displayName, String classPath) {
-        this.displayName = displayName;
-        this.classPath = classPath;
-    }
-
-    public String getClassPath() {
-        return this.classPath;
-    }
-
-    public String getDisplayName() {
-        return this.displayName;
-    }
-
-    public static LanguageOption fromDisplayName(String displayName) {
-        return Arrays.stream(LanguageOption.values()).filter(languageOption -> languageOption.displayName.equalsIgnoreCase(displayName)).findFirst()
-                .orElse(getDefault());
-    }
-
-    public static Collection<String> getAllDisplayNames() {
-        return Arrays.stream(LanguageOption.values()).map(languageOption -> languageOption.displayName).toList();
-    }
-
-    public static LanguageOption getDefault() {
-        return LanguageOption.JAVA;
-    }
-}

+ 2 - 1
jplag/src/test/java/de/jplag/InvalidSubmissionTest.java

@@ -30,7 +30,8 @@ class InvalidSubmissionTest extends TestBase {
             File errorFolder = new File(Path.of(BASE_PATH, SAMPLE_NAME, "errors", "java").toString());
             assertTrue(errorFolder.exists());
             String[] errorSubmissions = errorFolder.list();
-            Arrays.sort(errorSubmissions); // File systems don't promise alphabetical order.
+            if (errorSubmissions != null)
+                Arrays.sort(errorSubmissions); // File systems don't promise alphabetical order.
             deleteDirectory(errorFolder.getParentFile());
             assertArrayEquals(new String[] {"A", "B"}, errorSubmissions);
         }

+ 2 - 2
jplag/src/test/java/de/jplag/TestBase.java

@@ -7,8 +7,8 @@ import java.util.StringJoiner;
 import java.util.function.Consumer;
 
 import de.jplag.exceptions.ExitException;
+import de.jplag.java.Language;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 import de.jplag.options.Verbosity;
 
 public abstract class TestBase {
@@ -48,7 +48,7 @@ public abstract class TestBase {
     }
 
     protected JPlagResult runJPlag(List<String> newPaths, List<String> oldPaths, Consumer<JPlagOptions> customization) throws ExitException {
-        JPlagOptions options = new JPlagOptions(newPaths, oldPaths, LanguageOption.JAVA);
+        JPlagOptions options = new JPlagOptions(newPaths, oldPaths, Language.IDENTIFIER);
         options.setVerbosity(Verbosity.LONG);
         customization.accept(options);
         JPlag jplag = new JPlag(options);

+ 13 - 8
jplag/src/test/java/de/jplag/special/TokenPrinterTest.java

@@ -10,29 +10,34 @@ import org.junit.jupiter.api.Test;
 import de.jplag.*;
 import de.jplag.exceptions.ExitException;
 import de.jplag.options.JPlagOptions;
-import de.jplag.options.LanguageOption;
 
 /**
  * Special test case the does not really test anything but prints the tokens and the corresponding line of code.
  * @author Timur Saglam
  */
-public class TokenPrinterTest extends TestBase {
+class TokenPrinterTest extends TestBase {
     private static final String LARGE_SPACE = "   ";
     private static final String LINE = "------------------";
 
     private static final int MIN_TOKEN_MATCH = 5;
     private static final String PRINTER_FOLDER = "PRINTER"; // in the folder 'jplag/src/test/resources/samples'
 
-    @Disabled
+    private static final String LANGUAGE_CPP = de.jplag.cpp.Language.IDENTIFIER;
+    private static final String LANGUAGE_R = de.jplag.rlang.Language.IDENTIFIER;
+    private static final String LANGUAGE_KOTLIN = de.jplag.kotlin.Language.IDENTIFIER;
+
+    private static final String LANGUAGE_GO = de.jplag.golang.Language.IDENTIFIER;
+
+    @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printCPPFiles() {
         printSubmissions(options -> {
-            options.setLanguageOption(LanguageOption.C_CPP);
+            options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_CPP).orElseThrow());
             options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
         });
     }
 
-    @Disabled
+    @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printJavaFiles() {
         printSubmissions(options -> {
@@ -44,7 +49,7 @@ public class TokenPrinterTest extends TestBase {
     @Test
     void printRLangFiles() {
         printSubmissions(options -> {
-            options.setLanguageOption(LanguageOption.R_LANG);
+            options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_R).orElseThrow());
             options.setMinimumTokenMatch(MIN_TOKEN_MATCH); // for printing also allow small files
         });
     }
@@ -52,13 +57,13 @@ public class TokenPrinterTest extends TestBase {
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printGoFiles() {
-        printSubmissions(options -> options.setLanguageOption(LanguageOption.GO_LANG));
+        printSubmissions(options -> options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_GO).orElseThrow()));
     }
 
     @Disabled("Not a meaningful test, used for designing the token set")
     @Test
     void printKotlinFiles() {
-        printSubmissions(options -> options.setLanguageOption(LanguageOption.KOTLIN));
+        printSubmissions(options -> options.setLanguage(LanguageLoader.getLanguage(LANGUAGE_KOTLIN).orElseThrow()));
     }
 
     private void printSubmissions(Consumer<JPlagOptions> optionsCustomization) {