diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index ac79e68c0..e64b81118 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -1,21 +1,8 @@ package de.jplag.cli; -import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING; -import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; -import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; - -import java.awt.Desktop; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.net.URI; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; @@ -23,173 +10,102 @@ import de.jplag.JPlag; import de.jplag.JPlagResult; -import de.jplag.Language; import de.jplag.cli.logger.CollectedLoggerFactory; import de.jplag.cli.logger.TongfeiProgressBarProvider; -import de.jplag.cli.server.ReportViewer; -import de.jplag.clustering.ClusteringOptions; -import de.jplag.clustering.Preprocessing; +import de.jplag.cli.picocli.CliInputHandler; import de.jplag.exceptions.ExitException; import de.jplag.logging.ProgressBarLogger; -import de.jplag.merging.MergingOptions; import de.jplag.options.JPlagOptions; -import de.jplag.options.LanguageOption; -import de.jplag.options.LanguageOptions; -import de.jplag.reporting.reportobject.ReportObjectFactory; - -import picocli.CommandLine; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Model.OptionSpec; -import picocli.CommandLine.ParseResult; /** * Command line interface class, allows using via command line. * @see CLI#main(String[]) */ public final class CLI { - private static final Logger logger = LoggerFactory.getLogger(CLI.class); - private static final Random RANDOM = new SecureRandom(); - - private static final String CREDITS = "Created by IPD Tichy, Guido Malpohl, and others. Maintained by Timur Saglam and Sebastian Hahner. Logo by Sandro Koch."; - - private static final String[] DESCRIPTIONS = {"Detecting Software Plagiarism", "Software-Archaeological Playground", "Since 1996", - "Scientifically Published", "Maintained by SDQ", "RIP Structure and Table", "What else?", "You have been warned!", "Since Java 1.0", - "More Abstract than Tree", "Students Nightmare", "No, changing variable names does not work...", "The tech is out there!", - "Developed by plagiarism experts.", "State of the Art Obfuscation Resilience", "www.helmholtz.software/software/jplag"}; + private static final String DEFAULT_FILE_ENDING = ".zip"; - private static final String OPTION_LIST_HEADING = "Parameter descriptions: "; + private final CliInputHandler inputHandler; - private final CommandLine commandLine; - private final CliOptions options; + /** + * Creates a cli. + * @param args The command line arguments + */ + public CLI(String[] args) { + this.inputHandler = new CliInputHandler(args); + } - private static final String IMPOSSIBLE_EXCEPTION = "This should not have happened." - + " Please create an issue on github (https://github.com/jplag/JPlag/issues) with the entire output."; - private static final String UNKOWN_LANGAUGE_EXCEPTION = "Language %s does not exists. Available languages are: %s"; + /** + * Executes the cli + * @throws ExitException If anything on the side of JPlag goes wrong + * @throws IOException If any files did not work + */ + public void executeCli() throws ExitException, IOException { + logger.debug("Your version of JPlag is {}", JPlag.JPLAG_VERSION); - private static final String DESCRIPTION_PATTERN = "%nJPlag - %s%n%s%n%n"; + if (!this.inputHandler.parse()) { + ProgressBarLogger.setProgressBarProvider(new TongfeiProgressBarProvider()); - private static final String DEFAULT_FILE_ENDING = ".zip"; + switch (this.inputHandler.getCliOptions().mode) { + case RUN -> runJPlag(); + case VIEW -> runViewer(null); + case RUN_AND_VIEW -> runViewer(runJPlag()); + } + } + } /** - * Main class for using JPlag via the CLI. - * @param args are the CLI arguments that will be passed to JPlag. + * Executes the cli and handles the exceptions that might occur. + * @return true, if an exception has been caught. */ - public static void main(String[] args) { - try { - logger.debug("Your version of JPlag is {}", JPlag.JPLAG_VERSION); - - CLI cli = new CLI(); + public boolean executeCliAndHandleErrors() { + boolean hadErrors = false; - ParseResult parseResult = cli.parseOptions(args); - - if (!parseResult.isUsageHelpRequested() && !(parseResult.subcommand() != null && parseResult.subcommand().isUsageHelpRequested())) { - ProgressBarLogger.setProgressBarProvider(new TongfeiProgressBarProvider()); - switch (cli.options.mode) { - case RUN -> cli.runJPlag(parseResult); - case VIEW -> cli.runViewer(null); - case RUN_AND_VIEW -> cli.runViewer(cli.runJPlag(parseResult)); - } - } - } catch (ExitException | IOException exception) { // do not pass exceptions here to keep log clean + try { + this.executeCli(); + } catch (IOException | ExitException exception) { if (exception.getCause() != null) { logger.error("{} - {}", exception.getMessage(), exception.getCause().getMessage()); } else { logger.error(exception.getMessage()); } - + hadErrors = true; + } finally { finalizeLogger(); - System.exit(1); } + + return hadErrors; } /** - * Creates a new instance + * Runs JPlag and returns the file the result has been written to + * @return The file containing the result + * @throws ExitException If JPlag threw an exception + * @throws FileNotFoundException If the file could not be written */ - public CLI() { - this.options = new CliOptions(); - this.commandLine = new CommandLine(options); - - this.commandLine.setHelpFactory(new HelpFactory()); - - this.commandLine.getHelpSectionMap().put(SECTION_KEY_OPTION_LIST, help -> help.optionList().lines().map(it -> { - if (it.startsWith(" -")) { - return " " + it; - } - return it; - }).collect(Collectors.joining(System.lineSeparator()))); - - buildSubcommands().forEach(commandLine::addSubcommand); - - this.commandLine.getHelpSectionMap().put(SECTION_KEY_SYNOPSIS, help -> help.synopsis(help.synopsisHeadingLength()) + generateDescription()); - this.commandLine.getHelpSectionMap().put(SECTION_KEY_DESCRIPTION_HEADING, help -> OPTION_LIST_HEADING); - this.commandLine.setAllowSubcommandsAsOptionParameters(true); - } + public File runJPlag() throws ExitException, FileNotFoundException { + JPlagOptionsBuilder optionsBuilder = new JPlagOptionsBuilder(this.inputHandler); + JPlagOptions options = optionsBuilder.buildOptions(); + JPlagResult result = JPlagRunner.runJPlag(options); - public File runJPlag(ParseResult parseResult) throws ExitException, FileNotFoundException { - JPlagOptions jplagOptions = buildOptionsFromArguments(parseResult); - JPlagResult result = JPlag.run(jplagOptions); File target = new File(getResultFilePath()); - ReportObjectFactory reportObjectFactory = new ReportObjectFactory(target); - reportObjectFactory.createAndSaveReport(result); - logger.info("Successfully written the result: {}", target.getPath()); - logger.info("View the result using --mode or at: https://jplag.github.io/JPlag/"); - OutputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.options); - return target; - } - - public void runViewer(File zipFile) throws IOException { - ReportViewer reportViewer = new ReportViewer(zipFile, this.options.advanced.port); - int port = reportViewer.start(); - logger.info("ReportViewer started on port http://localhost:{}", port); - Desktop.getDesktop().browse(URI.create("http://localhost:" + port + "/")); - - System.out.println("Press Enter key to exit..."); - System.in.read(); - reportViewer.stop(); - } - - private List buildSubcommands() { - return LanguageLoader.getAllAvailableLanguages().values().stream().map(language -> { - CommandSpec command = CommandSpec.create().name(language.getIdentifier()); + OutputFileGenerator.generateJPlagResultZip(result, target); + OutputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.inputHandler.getCliOptions()); - for (LanguageOption option : language.getOptions().getOptionsAsList()) { - command.addOption(OptionSpec.builder(option.getNameAsUnixParameter()).type(option.getType().getJavaType()) - .description(option.getDescription()).build()); - } - command.mixinStandardHelpOptions(true); - command.addPositional( - CommandLine.Model.PositionalParamSpec.builder().type(List.class).auxiliaryTypes(File.class).hidden(true).required(false).build()); - - return command; - }).toList(); + return target; } /** - * Parses the options from the given command line arguments. Also prints help pages when requested. - * @param args The command line arguments - * @return the parse result generated by picocli + * Runs the report viewer using the given file as the default result.zip + * @param zipFile The zip file to pass to the viewer. Can be null, if no result should be opened by default + * @throws IOException If something went wrong with the internal server */ - public ParseResult parseOptions(String... args) throws CliException { - try { - ParseResult result = commandLine.parseArgs(args); - if (result.isUsageHelpRequested() || (result.subcommand() != null && result.subcommand().isUsageHelpRequested())) { - commandLine.getExecutionStrategy().execute(result); - } - return result; - } catch (CommandLine.ParameterException e) { - if (e.getArgSpec() != null && e.getArgSpec().isOption() && Arrays.asList(((OptionSpec) e.getArgSpec()).names()).contains("-l")) { - throw new CliException(String.format(UNKOWN_LANGAUGE_EXCEPTION, e.getValue(), - String.join(", ", LanguageLoader.getAllAvailableLanguageIdentifiers()))); - } - throw new CliException("Error during parsing", e); - } catch (CommandLine.PicocliException e) { - throw new CliException("Error during parsing", e); - } + public void runViewer(File zipFile) throws IOException { + JPlagRunner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); } - private static void finalizeLogger() { + private void finalizeLogger() { ILoggerFactory factory = LoggerFactory.getILoggerFactory(); if (!(factory instanceof CollectedLoggerFactory collectedLoggerFactory)) { return; @@ -197,94 +113,8 @@ private static void finalizeLogger() { collectedLoggerFactory.finalizeInstances(); } - /** - * Builds an options instance from parsed options. - * @return the newly built options - */ - public JPlagOptions buildOptionsFromArguments(ParseResult parseResult) throws CliException { - Set submissionDirectories = new HashSet<>(List.of(this.options.rootDirectory)); - Set oldSubmissionDirectories = Set.of(this.options.oldDirectories); - List suffixes = List.of(this.options.advanced.suffixes); - submissionDirectories.addAll(List.of(this.options.newDirectories)); - - if (parseResult.subcommand() != null && parseResult.subcommand().hasMatchedPositional(0)) { - submissionDirectories.addAll(parseResult.subcommand().matchedPositional(0).getValue()); - } - - ClusteringOptions clusteringOptions = getClusteringOptions(this.options); - MergingOptions mergingOptions = getMergingOptions(this.options); - - JPlagOptions jPlagOptions = new JPlagOptions(loadLanguage(parseResult), this.options.minTokenMatch, submissionDirectories, - oldSubmissionDirectories, null, this.options.advanced.subdirectory, suffixes, this.options.advanced.exclusionFileName, - JPlagOptions.DEFAULT_SIMILARITY_METRIC, this.options.advanced.similarityThreshold, this.options.shownComparisons, clusteringOptions, - this.options.advanced.debug, mergingOptions, this.options.normalize); - - String baseCodePath = this.options.baseCode; - File baseCodeDirectory = baseCodePath == null ? null : new File(baseCodePath); - if (baseCodeDirectory == null || baseCodeDirectory.exists()) { - return jPlagOptions.withBaseCodeSubmissionDirectory(baseCodeDirectory); - } - logger.warn("Using legacy partial base code API. Please migrate to new full path base code API."); - return jPlagOptions.withBaseCodeSubmissionName(baseCodePath); - } - - private Language loadLanguage(ParseResult result) throws CliException { - if (result.subcommand() == null) { - return this.options.language; - } - ParseResult subcommandResult = result.subcommand(); - Language language = LanguageLoader.getLanguage(subcommandResult.commandSpec().name()) - .orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION)); - LanguageOptions languageOptions = language.getOptions(); - languageOptions.getOptionsAsList().forEach(option -> { - if (subcommandResult.hasMatchedOption(option.getNameAsUnixParameter())) { - option.setValue(subcommandResult.matchedOptionValue(option.getNameAsUnixParameter(), null)); - } - }); - return language; - } - - private static ClusteringOptions getClusteringOptions(CliOptions options) { - ClusteringOptions clusteringOptions = new ClusteringOptions().withEnabled(!options.clustering.disable) - .withAlgorithm(options.clustering.enabled.algorithm).withSimilarityMetric(options.clustering.enabled.metric) - .withSpectralKernelBandwidth(options.clusterSpectralBandwidth).withSpectralGaussianProcessVariance(options.clusterSpectralNoise) - .withSpectralMinRuns(options.clusterSpectralMinRuns).withSpectralMaxRuns(options.clusterSpectralMaxRuns) - .withSpectralMaxKMeansIterationPerRun(options.clusterSpectralKMeansIterations) - .withAgglomerativeThreshold(options.clusterAgglomerativeThreshold) - .withAgglomerativeInterClusterSimilarity(options.clusterAgglomerativeInterClusterSimilarity); - - if (options.clusterPreprocessingNone) { - clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.NONE); - } - - if (options.clusterPreprocessingCdf) { - clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.CUMULATIVE_DISTRIBUTION_FUNCTION); - } - - if (options.clusterPreprocessingPercentile != 0) { - clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.PERCENTILE) - .withPreprocessorPercentile(options.clusterPreprocessingPercentile); - } - - if (options.clusterPreprocessingThreshold != 0) { - clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.THRESHOLD) - .withPreprocessorThreshold(options.clusterPreprocessingThreshold); - } - - return clusteringOptions; - } - - private static MergingOptions getMergingOptions(CliOptions options) { - return new MergingOptions(options.merging.enabled, options.merging.minimumNeighborLength, options.merging.maximumGapSize); - } - - private String generateDescription() { - var randomDescription = DESCRIPTIONS[RANDOM.nextInt(DESCRIPTIONS.length)]; - return String.format(DESCRIPTION_PATTERN, randomDescription, CREDITS); - } - private String getResultFilePath() { - String optionValue = this.options.resultFile; + String optionValue = this.inputHandler.getCliOptions().resultFile; if (optionValue.endsWith(DEFAULT_FILE_ENDING)) { return optionValue; } @@ -295,4 +125,11 @@ private String getResultFileBaseName() { String defaultOutputFile = getResultFilePath(); return defaultOutputFile.substring(0, defaultOutputFile.length() - DEFAULT_FILE_ENDING.length()); } + + public static void main(String[] args) { + CLI cli = new CLI(args); + if (cli.executeCliAndHandleErrors()) { + System.exit(1); + } + } } diff --git a/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java b/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java new file mode 100644 index 000000000..16c309f4b --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java @@ -0,0 +1,104 @@ +package de.jplag.cli; + +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jplag.cli.options.CliOptions; +import de.jplag.cli.picocli.CliInputHandler; +import de.jplag.clustering.ClusteringOptions; +import de.jplag.clustering.Preprocessing; +import de.jplag.merging.MergingOptions; +import de.jplag.options.JPlagOptions; + +/** + * Handles the building of JPlag options from the cli options + */ +public class JPlagOptionsBuilder { + private static final Logger logger = LoggerFactory.getLogger(JPlagOptionsBuilder.class); + + private final CliInputHandler cliInputHandler; + private final CliOptions cliOptions; + + /** + * @param cliInputHandler The cli handler containing the parsed cli options + */ + public JPlagOptionsBuilder(CliInputHandler cliInputHandler) { + this.cliInputHandler = cliInputHandler; + this.cliOptions = this.cliInputHandler.getCliOptions(); + } + + /** + * Builds the JPlag options + * @return The JPlag options + * @throws CliException If the input handler could properly parse everything. + */ + public JPlagOptions buildOptions() throws CliException { + Set submissionDirectories = new HashSet<>(List.of(this.cliOptions.rootDirectory)); + Set oldSubmissionDirectories = Set.of(this.cliOptions.oldDirectories); + List suffixes = List.of(this.cliOptions.advanced.suffixes); + submissionDirectories.addAll(List.of(this.cliOptions.newDirectories)); + submissionDirectories.addAll(this.cliInputHandler.getSubcommandSubmissionDirectories()); + + JPlagOptions jPlagOptions = initializeJPlagOptions(submissionDirectories, oldSubmissionDirectories, suffixes); + + String baseCodePath = this.cliOptions.baseCode; + File baseCodeDirectory = baseCodePath == null ? null : new File(baseCodePath); + if (baseCodeDirectory == null || baseCodeDirectory.exists()) { + return jPlagOptions.withBaseCodeSubmissionDirectory(baseCodeDirectory); + } + logger.error("Using legacy partial base code API. Please migrate to new full path base code API."); + return jPlagOptions.withBaseCodeSubmissionName(baseCodePath); + } + + private JPlagOptions initializeJPlagOptions(Set submissionDirectories, Set oldSubmissionDirectories, List suffixes) + throws CliException { + ClusteringOptions clusteringOptions = getClusteringOptions(); + MergingOptions mergingOptions = getMergingOptions(); + + return new JPlagOptions(this.cliInputHandler.getSelectedLanguage(), this.cliOptions.minTokenMatch, submissionDirectories, + oldSubmissionDirectories, null, this.cliOptions.advanced.subdirectory, suffixes, this.cliOptions.advanced.exclusionFileName, + JPlagOptions.DEFAULT_SIMILARITY_METRIC, this.cliOptions.advanced.similarityThreshold, this.cliOptions.shownComparisons, + clusteringOptions, this.cliOptions.advanced.debug, mergingOptions, this.cliOptions.normalize); + } + + private ClusteringOptions getClusteringOptions() { + ClusteringOptions clusteringOptions = new ClusteringOptions().withEnabled(!this.cliOptions.clustering.disable) + .withAlgorithm(this.cliOptions.clustering.enabled.algorithm).withSimilarityMetric(this.cliOptions.clustering.enabled.metric) + .withSpectralKernelBandwidth(this.cliOptions.clusterSpectralBandwidth) + .withSpectralGaussianProcessVariance(this.cliOptions.clusterSpectralNoise).withSpectralMinRuns(this.cliOptions.clusterSpectralMinRuns) + .withSpectralMaxRuns(this.cliOptions.clusterSpectralMaxRuns) + .withSpectralMaxKMeansIterationPerRun(this.cliOptions.clusterSpectralKMeansIterations) + .withAgglomerativeThreshold(this.cliOptions.clusterAgglomerativeThreshold) + .withAgglomerativeInterClusterSimilarity(this.cliOptions.clusterAgglomerativeInterClusterSimilarity); + + if (this.cliOptions.clusterPreprocessingNone) { + clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.NONE); + } + + if (this.cliOptions.clusterPreprocessingCdf) { + clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.CUMULATIVE_DISTRIBUTION_FUNCTION); + } + + if (this.cliOptions.clusterPreprocessingPercentile != 0) { + clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.PERCENTILE) + .withPreprocessorPercentile(this.cliOptions.clusterPreprocessingPercentile); + } + + if (this.cliOptions.clusterPreprocessingThreshold != 0) { + clusteringOptions = clusteringOptions.withPreprocessor(Preprocessing.THRESHOLD) + .withPreprocessorThreshold(this.cliOptions.clusterPreprocessingThreshold); + } + + return clusteringOptions; + } + + private MergingOptions getMergingOptions() { + return new MergingOptions(this.cliOptions.merging.enabled, this.cliOptions.merging.minimumNeighborLength, + this.cliOptions.merging.maximumGapSize); + } +} diff --git a/cli/src/main/java/de/jplag/cli/JPlagRunner.java b/cli/src/main/java/de/jplag/cli/JPlagRunner.java new file mode 100644 index 000000000..e53a0d03d --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -0,0 +1,52 @@ +package de.jplag.cli; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.net.URI; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jplag.JPlag; +import de.jplag.JPlagResult; +import de.jplag.cli.server.ReportViewer; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +/** + * Wraps the execution of the JPlag components + */ +public final class JPlagRunner { + private static final Logger logger = LoggerFactory.getLogger(JPlagRunner.class); + + private JPlagRunner() { + } + + /** + * Executes JPlag + * @param options The options to pass to JPlag + * @return The result returned by JPlag + * @throws ExitException If JPlag throws an error + */ + public static JPlagResult runJPlag(JPlagOptions options) throws ExitException { + return JPlag.run(options); + } + + /** + * Runs the internal server. Blocks until the server has stopped. + * @param zipFile The zip file to pass to the server. May be null. + * @param port The port to open the server on + * @throws IOException If the internal server throws an exception + */ + public static void runInternalServer(File zipFile, int port) throws IOException { + ReportViewer reportViewer = new ReportViewer(zipFile, port); + int actualPort = reportViewer.start(); + logger.info("ReportViewer started on port http://localhost:{}", actualPort); + Desktop.getDesktop().browse(URI.create("http://localhost:" + actualPort + "/")); + + System.out.println("Press Enter key to exit..."); + System.in.read(); + reportViewer.stop(); + } +} diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 028361346..4d9d41c84 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -1,23 +1,28 @@ package de.jplag.cli; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.jplag.JPlagResult; +import de.jplag.cli.options.CliOptions; import de.jplag.csv.comparisons.CsvComparisonOutput; +import de.jplag.reporting.reportobject.ReportObjectFactory; +/** + * Manages the creation of output files + */ public final class OutputFileGenerator { private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); private OutputFileGenerator() { - // Prevents default constructor } /** - * Exports the given result as csvs, if the csvExport is activated in the options. Both a full and an anonymized version + * Exports the given result as CSVs, if the csvExport is activated in the options. Both a full and an anonymized version * will be written. * @param result The result to export * @param outputRoot The root folder for the output @@ -33,4 +38,17 @@ public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOpt } } } + + /** + * Generates the JPLag result zip + * @param result The JPlag result + * @param outputFile The output file + * @throws FileNotFoundException If the file cannot be written + */ + public static void generateJPlagResultZip(JPlagResult result, File outputFile) throws FileNotFoundException { + ReportObjectFactory reportObjectFactory = new ReportObjectFactory(outputFile); + reportObjectFactory.createAndSaveReport(result); + logger.info("Successfully written the result: {}", outputFile.getPath()); + logger.info("View the result using --mode or at: https://jplag.github.io/JPlag/"); + } } diff --git a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java index 3be42c8cd..85b40629a 100644 --- a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java +++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java @@ -1,43 +1,22 @@ package de.jplag.cli.logger; import java.io.PrintStream; -import java.io.Serial; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.ConcurrentLinkedDeque; -import org.slf4j.helpers.FormattingTuple; -import org.slf4j.helpers.MarkerIgnoringBase; +import org.slf4j.Marker; +import org.slf4j.event.Level; import org.slf4j.helpers.MessageFormatter; -import org.slf4j.spi.LocationAwareLogger; /** - * This logger is able to collect errors and print them at the end. Mainly adopted from org.slf4j.impl.SimpleLogger - * @author Dominik Fuchss + * A logger implementation, that prints all errors during finalization */ -public final class CollectedLogger extends MarkerIgnoringBase { +public class CollectedLogger extends JPlagLoggerBase { + private static final int MAXIMUM_MESSAGE_LENGTH = 32; - @Serial - private static final long serialVersionUID = -1278670638921140275L; - - private static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT; - private static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT; - private static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT; - private static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT; - private static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT; - - /** - * The default log level that shall be used for external libraries (like Stanford Core NLP) - */ - private static final int LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = LOG_LEVEL_ERROR; - - private static final int CURRENT_LOG_LEVEL = LOG_LEVEL_INFO; - - /** - * The short name of this simple log instance - */ - private transient String shortLogName = null; + private static final PrintStream TARGET_STREAM = System.out; /** * Indicator whether finalization is in progress. @@ -47,262 +26,67 @@ public final class CollectedLogger extends MarkerIgnoringBase { private final transient SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh:mm:ss_SSS"); - private final ConcurrentLinkedDeque> allErrors = new ConcurrentLinkedDeque<>(); + private final ConcurrentLinkedDeque allErrors = new ConcurrentLinkedDeque<>(); - CollectedLogger(String name) { - this.name = name; + public CollectedLogger(String name) { + super(LOG_LEVEL_INFO, name); } - private void log(int level, String message, Throwable throwable) { - log(level, message, throwable, null); - } - - private void log(int level, String message, Throwable throwable, Date timeOfError) { - if (!isLevelEnabled(level)) { - return; - } + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String format, Object[] args, Throwable cause) { + String logMessage = prepareFormattedMessage(format, args); + LogEntry entry = new LogEntry(logMessage, cause, new Date(), level); if (level == LOG_LEVEL_ERROR && !isFinalizing) { - // Buffer errors for the final output - allErrors.add(new Triple<>(message, throwable, new Date())); - return; + allErrors.add(entry); + } else { + printLogEntry(entry); } + } - StringBuilder builder = new StringBuilder(32); + private String prepareFormattedMessage(String format, Object[] args) { + if (args == null) { + return format; + } - // Append date-time - builder.append(dateFormat.format(timeOfError == null ? new Date() : timeOfError)).append(' '); + return MessageFormatter.arrayFormat(format, args).getMessage(); + } - // Append current Level - builder.append('[').append(renderLevel(level)).append(']').append(' '); + private void printLogEntry(LogEntry entry) { + StringBuilder output = prepareLogOutput(entry); - // Append the name of the log instance - if (shortLogName == null) { - shortLogName = computeShortName(); + TARGET_STREAM.println(output); + if (entry.cause() != null) { + entry.cause().printStackTrace(TARGET_STREAM); } - builder.append(shortLogName).append(" - "); - // Append the message - builder.append(message); + TARGET_STREAM.flush(); + } - write(builder, throwable); + private StringBuilder prepareLogOutput(LogEntry entry) { + StringBuilder outputBuilder = new StringBuilder(MAXIMUM_MESSAGE_LENGTH); + outputBuilder.append(dateFormat.format(entry.timeOfLog())).append(' '); + outputBuilder.append('[').append(entry.logLevel().name()).append("] "); + outputBuilder.append(computeShortName()).append(" - "); + outputBuilder.append(entry.message()); + return outputBuilder; } void printAllErrorsForLogger() { this.isFinalizing = true; - // Copy errors to prevent infinite recursion - var errors = new ArrayList<>(this.allErrors); - if (errors.isEmpty()) { - return; + ArrayList errors = new ArrayList<>(this.allErrors); + + if (!errors.isEmpty()) { + info("Summary of all errors:"); + this.allErrors.removeAll(errors); + for (LogEntry errorEntry : errors) { + printLogEntry(errorEntry); + } } - this.allErrors.removeAll(errors); - - info("Summary of all Errors:"); - errors.forEach(error -> log(LOG_LEVEL_ERROR, error.first(), error.second(), error.third())); - isFinalizing = false; - } - - @SuppressWarnings("java:S106") - void write(StringBuilder buf, Throwable throwable) { - PrintStream targetStream = System.out; - - targetStream.println(buf.toString()); - writeThrowable(throwable, targetStream); - targetStream.flush(); - } - - private void writeThrowable(Throwable throwable, PrintStream targetStream) { - if (throwable != null) { - throwable.printStackTrace(targetStream); - } + this.isFinalizing = false; } private String computeShortName() { return name.substring(name.lastIndexOf(".") + 1); } - - private boolean isLevelEnabled(int logLevel) { - return logLevel >= (isJPlagLog() ? CURRENT_LOG_LEVEL : LOG_LEVEL_FOR_EXTERNAL_LIBRARIES); - } - - private boolean isJPlagLog() { - return this.name.startsWith("de.jplag."); - } - - private String renderLevel(int level) { - return switch (level) { - case LOG_LEVEL_TRACE -> "TRACE"; - case LOG_LEVEL_DEBUG -> "DEBUG"; - case LOG_LEVEL_INFO -> "INFO"; - case LOG_LEVEL_WARN -> "WARN"; - case LOG_LEVEL_ERROR -> "ERROR"; - default -> throw new IllegalStateException("Unrecognized level [" + level + "]"); - }; - } - - @Override - public boolean isTraceEnabled() { - return isLevelEnabled(LOG_LEVEL_TRACE); - } - - @Override - public void trace(String message) { - log(LOG_LEVEL_TRACE, message, null); - } - - @Override - public void trace(String format, Object param1) { - formatAndLog(LOG_LEVEL_TRACE, format, param1, null); - } - - @Override - public void trace(String format, Object param1, Object param2) { - formatAndLog(LOG_LEVEL_TRACE, format, param1, param2); - } - - @Override - public void trace(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_TRACE, format, argArray); - } - - @Override - public void trace(String message, Throwable t) { - log(LOG_LEVEL_TRACE, message, t); - } - - @Override - public boolean isDebugEnabled() { - return isLevelEnabled(LOG_LEVEL_DEBUG); - } - - @Override - public void debug(String message) { - log(LOG_LEVEL_DEBUG, message, null); - } - - @Override - public void debug(String format, Object param1) { - formatAndLog(LOG_LEVEL_DEBUG, format, param1, null); - } - - @Override - public void debug(String format, Object param1, Object param2) { - formatAndLog(LOG_LEVEL_DEBUG, format, param1, param2); - } - - @Override - public void debug(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_DEBUG, format, argArray); - } - - @Override - public void debug(String message, Throwable throwable) { - log(LOG_LEVEL_DEBUG, message, throwable); - } - - @Override - public boolean isInfoEnabled() { - return isLevelEnabled(LOG_LEVEL_INFO); - } - - @Override - public void info(String message) { - log(LOG_LEVEL_INFO, message, null); - } - - @Override - public void info(String format, Object arg) { - formatAndLog(LOG_LEVEL_INFO, format, arg, null); - } - - @Override - public void info(String format, Object arg1, Object arg2) { - formatAndLog(LOG_LEVEL_INFO, format, arg1, arg2); - } - - @Override - public void info(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_INFO, format, argArray); - } - - @Override - public void info(String message, Throwable throwable) { - log(LOG_LEVEL_INFO, message, throwable); - } - - @Override - public boolean isWarnEnabled() { - return isLevelEnabled(LOG_LEVEL_WARN); - } - - @Override - public void warn(String message) { - log(LOG_LEVEL_WARN, message, null); - } - - @Override - public void warn(String format, Object arg) { - formatAndLog(LOG_LEVEL_WARN, format, arg, null); - } - - @Override - public void warn(String format, Object arg1, Object arg2) { - formatAndLog(LOG_LEVEL_WARN, format, arg1, arg2); - } - - @Override - public void warn(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_WARN, format, argArray); - } - - @Override - public void warn(String message, Throwable throwable) { - log(LOG_LEVEL_WARN, message, throwable); - } - - @Override - public boolean isErrorEnabled() { - return isLevelEnabled(LOG_LEVEL_ERROR); - } - - @Override - public void error(String message) { - log(LOG_LEVEL_ERROR, message, null); - } - - @Override - public void error(String format, Object arg) { - formatAndLog(LOG_LEVEL_ERROR, format, arg, null); - } - - @Override - public void error(String format, Object arg1, Object arg2) { - formatAndLog(LOG_LEVEL_ERROR, format, arg1, arg2); - } - - @Override - public void error(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_ERROR, format, argArray); - } - - @Override - public void error(String message, Throwable throwable) { - log(LOG_LEVEL_ERROR, message, throwable); - } - - private void formatAndLog(int level, String format, Object arg1, Object arg2) { - if (!isLevelEnabled(level)) { - return; - } - FormattingTuple formattingTuple = MessageFormatter.format(format, arg1, arg2); - log(level, formattingTuple.getMessage(), formattingTuple.getThrowable()); - } - - private void formatAndLog(int level, String format, Object... arguments) { - if (!isLevelEnabled(level)) { - return; - } - FormattingTuple formattingTuple = MessageFormatter.arrayFormat(format, arguments); - log(level, formattingTuple.getMessage(), formattingTuple.getThrowable()); - } } diff --git a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java new file mode 100644 index 000000000..34a59fedc --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java @@ -0,0 +1,92 @@ +package de.jplag.cli.logger; + +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.helpers.AbstractLogger; + +/** + * Handles the enabled log levels for SLF4J. + */ +public abstract class JPlagLoggerBase extends AbstractLogger { + protected static final Level LOG_LEVEL_TRACE = Level.TRACE; + protected static final Level LOG_LEVEL_DEBUG = Level.DEBUG; + protected static final Level LOG_LEVEL_INFO = Level.INFO; + protected static final Level LOG_LEVEL_WARN = Level.WARN; + protected static final Level LOG_LEVEL_ERROR = Level.ERROR; + + private static final Level LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = LOG_LEVEL_ERROR; + + private final Level currentLogLevel; + + /** + * @param currentLogLevel The current log level + * @param name The name of the logger + */ + protected JPlagLoggerBase(Level currentLogLevel, String name) { + this.currentLogLevel = currentLogLevel; + this.name = name; + } + + @Override + public boolean isTraceEnabled() { + return isLogLevelEnabled(LOG_LEVEL_TRACE); + } + + @Override + public boolean isTraceEnabled(Marker marker) { + return isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return isLogLevelEnabled(LOG_LEVEL_DEBUG); + } + + @Override + public boolean isDebugEnabled(Marker marker) { + return isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return isLogLevelEnabled(LOG_LEVEL_INFO); + } + + @Override + public boolean isInfoEnabled(Marker marker) { + return isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return isLogLevelEnabled(LOG_LEVEL_WARN); + } + + @Override + public boolean isWarnEnabled(Marker marker) { + return isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return isLogLevelEnabled(LOG_LEVEL_ERROR); + } + + @Override + public boolean isErrorEnabled(Marker marker) { + return isErrorEnabled(); + } + + private boolean isLogLevelEnabled(Level logLevel) { + return logLevel.toInt() >= (isJPlagLog() ? this.currentLogLevel.toInt() : LOG_LEVEL_FOR_EXTERNAL_LIBRARIES.toInt()); + } + + private boolean isJPlagLog() { + return this.name.startsWith("de.jplag."); + } + + @Override + protected String getFullyQualifiedCallerName() { + return null; // does not seem to be used by anything, but is required by SLF4J + } +} diff --git a/cli/src/main/java/de/jplag/cli/logger/LogEntry.java b/cli/src/main/java/de/jplag/cli/logger/LogEntry.java new file mode 100644 index 000000000..86d94d2ce --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/logger/LogEntry.java @@ -0,0 +1,15 @@ +package de.jplag.cli.logger; + +import java.util.Date; + +import org.slf4j.event.Level; + +/** + * Holds a log entry for later usage + * @param message The message of the log + * @param cause The cause of the log + * @param timeOfLog The time of the log + * @param logLevel The level of the log entry + */ +public record LogEntry(String message, Throwable cause, Date timeOfLog, Level logLevel) { +} diff --git a/cli/src/main/java/de/jplag/cli/logger/Triple.java b/cli/src/main/java/de/jplag/cli/logger/Triple.java deleted file mode 100644 index 76a309063..000000000 --- a/cli/src/main/java/de/jplag/cli/logger/Triple.java +++ /dev/null @@ -1,4 +0,0 @@ -package de.jplag.cli.logger; - -public record Triple(A first, B second, C third) { -} diff --git a/cli/src/main/java/de/jplag/cli/CliOptions.java b/cli/src/main/java/de/jplag/cli/options/CliOptions.java similarity index 99% rename from cli/src/main/java/de/jplag/cli/CliOptions.java rename to cli/src/main/java/de/jplag/cli/options/CliOptions.java index 384a2d41c..999cd5635 100644 --- a/cli/src/main/java/de/jplag/cli/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/options/CliOptions.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.options; import java.io.File; diff --git a/cli/src/main/java/de/jplag/cli/JPlagMode.java b/cli/src/main/java/de/jplag/cli/options/JPlagMode.java similarity index 91% rename from cli/src/main/java/de/jplag/cli/JPlagMode.java rename to cli/src/main/java/de/jplag/cli/options/JPlagMode.java index 402a18b58..8d1607d46 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagMode.java +++ b/cli/src/main/java/de/jplag/cli/options/JPlagMode.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.options; /** * The mode JPlag runs in. This influences which steps JPlag will execute. diff --git a/cli/src/main/java/de/jplag/cli/LanguageCandidates.java b/cli/src/main/java/de/jplag/cli/options/LanguageCandidates.java similarity index 92% rename from cli/src/main/java/de/jplag/cli/LanguageCandidates.java rename to cli/src/main/java/de/jplag/cli/options/LanguageCandidates.java index 715d504ea..e1c764b8f 100644 --- a/cli/src/main/java/de/jplag/cli/LanguageCandidates.java +++ b/cli/src/main/java/de/jplag/cli/options/LanguageCandidates.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.options; import java.util.ArrayList; diff --git a/cli/src/main/java/de/jplag/cli/LanguageConverter.java b/cli/src/main/java/de/jplag/cli/options/LanguageConverter.java similarity index 91% rename from cli/src/main/java/de/jplag/cli/LanguageConverter.java rename to cli/src/main/java/de/jplag/cli/options/LanguageConverter.java index 0a2523b95..9f92ec944 100644 --- a/cli/src/main/java/de/jplag/cli/LanguageConverter.java +++ b/cli/src/main/java/de/jplag/cli/options/LanguageConverter.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.options; import de.jplag.Language; diff --git a/cli/src/main/java/de/jplag/cli/LanguageLoader.java b/cli/src/main/java/de/jplag/cli/options/LanguageLoader.java similarity index 99% rename from cli/src/main/java/de/jplag/cli/LanguageLoader.java rename to cli/src/main/java/de/jplag/cli/options/LanguageLoader.java index 2ee1c815b..408247638 100644 --- a/cli/src/main/java/de/jplag/cli/LanguageLoader.java +++ b/cli/src/main/java/de/jplag/cli/options/LanguageLoader.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.options; import java.util.Collections; import java.util.Map; diff --git a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java new file mode 100644 index 000000000..2062b6380 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java @@ -0,0 +1,170 @@ +package de.jplag.cli.picocli; + +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; + +import java.io.File; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import de.jplag.Language; +import de.jplag.cli.CliException; +import de.jplag.cli.options.CliOptions; +import de.jplag.cli.options.LanguageLoader; +import de.jplag.options.LanguageOption; +import de.jplag.options.LanguageOptions; + +import picocli.CommandLine; +import picocli.CommandLine.ParseResult; + +/** + * Handles the parsing of the command line arguments + */ +public class CliInputHandler { + private static final String OPTION_LIST_HEADING = "Parameter descriptions: "; + + private static final String UNKNOWN_LANGUAGE_EXCEPTION = "Language %s does not exists. Available languages are: %s"; + private static final String IMPOSSIBLE_EXCEPTION = "This should not have happened." + + " Please create an issue on github (https://github.com/jplag/JPlag/issues) with the entire output."; + + private static final String[] DESCRIPTIONS = {"Detecting Software Plagiarism", "Software-Archaeological Playground", "Since 1996", + "Scientifically Published", "Maintained by SDQ", "RIP Structure and Table", "What else?", "You have been warned!", "Since Java 1.0", + "More Abstract than Tree", "Students Nightmare", "No, changing variable names does not work...", "The tech is out there!", + "Developed by plagiarism experts.", "State of the Art Obfuscation Resilience", "www.helmholtz.software/software/jplag"}; + private static final String DESCRIPTION_PATTERN = "%nJPlag - %s%n%s%n%n"; + private static final String CREDITS = "Created by IPD Tichy, Guido Malpohl, and others. Maintained by Timur Saglam and Sebastian Hahner. Logo by Sandro Koch."; + + private static final String PARAMETER_SHORT_PREFIX = " -"; + private static final String PARAMETER_SHORT_ADDITIONAL_INDENT = " "; + + private static final Random RANDOM = new SecureRandom(); + + private final String[] args; + private final CliOptions options; + private final CommandLine commandLine; + + private ParseResult parseResult; + + /** + * Creates a new handler. Before using it you need to call {@link #parse()} + * @param args The arguments. + */ + public CliInputHandler(String[] args) { + this.args = args; + this.options = new CliOptions(); + this.commandLine = buildCommandLine(); + } + + private CommandLine buildCommandLine() { + CommandLine cli = new CommandLine(this.options); + cli.setHelpFactory(new HelpFactory()); + + cli.getHelpSectionMap().put(SECTION_KEY_OPTION_LIST, help -> help.optionList().lines().map(it -> { + if (it.startsWith(PARAMETER_SHORT_PREFIX)) { + return PARAMETER_SHORT_ADDITIONAL_INDENT + it; + } + return it; + }).collect(Collectors.joining(System.lineSeparator()))); + + buildSubcommands().forEach(cli::addSubcommand); + + cli.getHelpSectionMap().put(SECTION_KEY_SYNOPSIS, help -> help.synopsis(help.synopsisHeadingLength()) + generateDescription()); + cli.getHelpSectionMap().put(SECTION_KEY_DESCRIPTION_HEADING, help -> OPTION_LIST_HEADING); + cli.setAllowSubcommandsAsOptionParameters(true); + + return cli; + } + + private List buildSubcommands() { + return LanguageLoader.getAllAvailableLanguages().values().stream().map(language -> { + CommandLine.Model.CommandSpec command = CommandLine.Model.CommandSpec.create().name(language.getIdentifier()); + + for (LanguageOption option : language.getOptions().getOptionsAsList()) { + command.addOption(CommandLine.Model.OptionSpec.builder(option.getNameAsUnixParameter()).type(option.getType().getJavaType()) + .description(option.getDescription()).build()); + } + command.mixinStandardHelpOptions(true); + command.addPositional( + CommandLine.Model.PositionalParamSpec.builder().type(List.class).auxiliaryTypes(File.class).hidden(true).required(false).build()); + + return command; + }).toList(); + } + + /** + * Parses the cli parameters and prints the usage help if requested. + * @return true, if the usage help has been requested. In this case the program should stop. + * @throws CliException If something went wrong during parsing. + */ + public boolean parse() throws CliException { + try { + this.parseResult = this.commandLine.parseArgs(args); + if (this.parseResult.isUsageHelpRequested() + || (this.parseResult.subcommand() != null && this.parseResult.subcommand().isUsageHelpRequested())) { + commandLine.getExecutionStrategy().execute(this.parseResult); + return true; + } + } catch (CommandLine.ParameterException e) { + if (e.getArgSpec() != null && e.getArgSpec().isOption() + && Arrays.asList(((CommandLine.Model.OptionSpec) e.getArgSpec()).names()).contains("-l")) { + throw new CliException(String.format(UNKNOWN_LANGUAGE_EXCEPTION, e.getValue(), + String.join(", ", LanguageLoader.getAllAvailableLanguageIdentifiers()))); + } + throw new CliException("Error during parsing", e); + } catch (CommandLine.PicocliException e) { + throw new CliException("Error during parsing", e); + } + return false; + } + + /** + * If {@link #parse()} has not been called yet, this will be empty, otherwise it will be a valid object. + * @return The parsed cli options. + */ + public CliOptions getCliOptions() { + return options; + } + + /** + * Resolves the language selected by the cli arguments. + * @return The selected language + * @throws CliException In the event the language cannot be resolved. Should not happen under normal circumstances. + */ + public Language getSelectedLanguage() throws CliException { + if (this.parseResult.subcommand() == null) { + return this.options.language; + } + + ParseResult subcommand = this.parseResult.subcommand(); + + Language language = LanguageLoader.getLanguage(subcommand.commandSpec().name()).orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION)); + + LanguageOptions languageOptions = language.getOptions(); + languageOptions.getOptionsAsList().forEach(option -> { + if (subcommand.hasMatchedOption(option.getNameAsUnixParameter())) { + option.setValue(subcommand.matchedOptionValue(option.getNameAsUnixParameter(), null)); + } + }); + return language; + } + + /** + * @return The submission directories configured for the subcommand, if one has been given. + */ + public List getSubcommandSubmissionDirectories() { + if (this.parseResult.subcommand() != null && this.parseResult.subcommand().hasMatchedPositional(0)) { + return this.parseResult.subcommand().matchedPositional(0).getValue(); + } + return Collections.emptyList(); + } + + private String generateDescription() { + var randomDescription = DESCRIPTIONS[RANDOM.nextInt(DESCRIPTIONS.length)]; + return String.format(DESCRIPTION_PATTERN, randomDescription, CREDITS); + } +} diff --git a/cli/src/main/java/de/jplag/cli/CustomHelp.java b/cli/src/main/java/de/jplag/cli/picocli/CustomHelp.java similarity index 96% rename from cli/src/main/java/de/jplag/cli/CustomHelp.java rename to cli/src/main/java/de/jplag/cli/picocli/CustomHelp.java index 368f358ce..992648697 100644 --- a/cli/src/main/java/de/jplag/cli/CustomHelp.java +++ b/cli/src/main/java/de/jplag/cli/picocli/CustomHelp.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.picocli; import picocli.CommandLine; diff --git a/cli/src/main/java/de/jplag/cli/HelpFactory.java b/cli/src/main/java/de/jplag/cli/picocli/HelpFactory.java similarity index 92% rename from cli/src/main/java/de/jplag/cli/HelpFactory.java rename to cli/src/main/java/de/jplag/cli/picocli/HelpFactory.java index 53aa208b7..a587f3803 100644 --- a/cli/src/main/java/de/jplag/cli/HelpFactory.java +++ b/cli/src/main/java/de/jplag/cli/picocli/HelpFactory.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.picocli; import picocli.CommandLine; diff --git a/cli/src/main/java/de/jplag/cli/ParamLabelRenderer.java b/cli/src/main/java/de/jplag/cli/picocli/ParamLabelRenderer.java similarity index 98% rename from cli/src/main/java/de/jplag/cli/ParamLabelRenderer.java rename to cli/src/main/java/de/jplag/cli/picocli/ParamLabelRenderer.java index 2d815af30..bd23f8735 100644 --- a/cli/src/main/java/de/jplag/cli/ParamLabelRenderer.java +++ b/cli/src/main/java/de/jplag/cli/picocli/ParamLabelRenderer.java @@ -1,4 +1,4 @@ -package de.jplag.cli; +package de.jplag.cli.picocli; import java.util.Arrays; import java.util.List; diff --git a/cli/src/main/java/de/jplag/cli/server/ReportViewer.java b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java index 6e861c926..b571be8ec 100644 --- a/cli/src/main/java/de/jplag/cli/server/ReportViewer.java +++ b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java @@ -89,7 +89,7 @@ public void stop() { /** * Do not call manually. Called by the running web server. - * @param exchange The http reqest + * @param exchange The http request * @throws IOException If the IO handling goes wrong */ @Override diff --git a/cli/src/main/java/de/jplag/cli/server/Routing.java b/cli/src/main/java/de/jplag/cli/server/Routing.java index a6152a031..e0a001dbf 100644 --- a/cli/src/main/java/de/jplag/cli/server/Routing.java +++ b/cli/src/main/java/de/jplag/cli/server/Routing.java @@ -3,7 +3,7 @@ import com.sun.net.httpserver.HttpExchange; /** - * Handles the data for a url prefix. + * Handles the data for an url prefix. */ public interface Routing { /** @@ -15,7 +15,7 @@ default HttpRequestMethod[] allowedMethods() { /** * Gets the data for the given url - * @param subPath The remaining suffix of the url, that is not jet interpreted + * @param subPath The remaining suffix of the url, that is not yet interpreted * @param request The original http request * @param viewer The current report viewer * @return The data to respond with diff --git a/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java b/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java index b5503791b..49848a96b 100644 --- a/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java +++ b/cli/src/test/java/de/jplag/cli/ArgumentBuilder.java @@ -138,7 +138,7 @@ public ArgumentBuilder minTokens(int count) { } /** - * Sets the similarity threshold as a string, so invalid values can be configures + * Sets the similarity threshold as a string, so invalid values can be configured * @param value The value * @return self reference */ diff --git a/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java index eb6ffca8c..3946dbe45 100644 --- a/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java +++ b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java @@ -1,9 +1,8 @@ package de.jplag.cli; +import de.jplag.cli.picocli.CliInputHandler; import de.jplag.options.JPlagOptions; -import picocli.CommandLine; - /** * Test base for tests regarding the {@link CLI}. Solely tests if the arguments set via the command line interface are * propagated correctly into options. JPlag is not executed for the different command line arguments, thus these tests @@ -14,7 +13,6 @@ public abstract class CommandLineInterfaceTest { protected static final String CURRENT_DIRECTORY = "."; protected static final double DELTA = 1E-5; - protected CLI cli; protected JPlagOptions options; /** @@ -32,13 +30,13 @@ protected ArgumentBuilder defaultArguments() { } /** - * Builds {@link JPlagOptions} via the command line interface. Sets {@link CommandLineInterfaceTest#cli} + * Builds {@link JPlagOptions} via the command line interface. * @param builder The argument builder containing the values to pass to the cli */ protected void buildOptionsFromCLI(ArgumentBuilder builder) throws CliException { - cli = new CLI(); - CommandLine.ParseResult result = cli.parseOptions(builder.getArgumentsAsArray()); - options = cli.buildOptionsFromArguments(result); + CliInputHandler inputHandler = new CliInputHandler(builder.getArgumentsAsArray()); + inputHandler.parse(); + this.options = new JPlagOptionsBuilder(inputHandler).buildOptions(); } } diff --git a/cli/src/test/java/de/jplag/cli/CustomHelpTests.java b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java index 3abde7505..9fecd3f3b 100644 --- a/cli/src/test/java/de/jplag/cli/CustomHelpTests.java +++ b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java @@ -4,6 +4,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.jplag.cli.picocli.CustomHelp; +import de.jplag.cli.picocli.HelpFactory; +import de.jplag.cli.picocli.ParamLabelRenderer; + import picocli.CommandLine; /** @@ -26,7 +30,7 @@ void setup() { */ @Test void testReturnsCustomRenderer() { - Assertions.assertTrue(this.help.parameterLabelRenderer() instanceof ParamLabelRenderer, + Assertions.assertInstanceOf(ParamLabelRenderer.class, this.help.parameterLabelRenderer(), "The custom help object returned the wrong ParamLabelRenderer type."); } } diff --git a/cli/src/test/java/de/jplag/cli/LanguageTest.java b/cli/src/test/java/de/jplag/cli/LanguageTest.java index 6561c8034..a23911284 100644 --- a/cli/src/test/java/de/jplag/cli/LanguageTest.java +++ b/cli/src/test/java/de/jplag/cli/LanguageTest.java @@ -9,6 +9,8 @@ import org.junit.jupiter.api.Test; import de.jplag.Language; +import de.jplag.cli.options.CliOptions; +import de.jplag.cli.options.LanguageLoader; class LanguageTest extends CommandLineInterfaceTest { diff --git a/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java b/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java index f10a2d350..817b84fef 100644 --- a/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java +++ b/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import de.jplag.cli.picocli.ParamLabelRenderer; + import picocli.CommandLine; /** diff --git a/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java b/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java index 318c00db2..dc0bf86a7 100644 --- a/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java +++ b/cli/src/test/java/de/jplag/cli/server/RoutingTreeTest.java @@ -54,7 +54,7 @@ void testPartialPathRoute() { } @Test - void testPartialPathRouteWithSubpath() { + void testPartialPathRouteWithSubPath() { RoutingTree routingTree = new RoutingTree(); routingTree.insertRouting("/path/", new TestRouting("/path/")); routingTree.insertRouting("/path/subPath/a.html", new TestRouting("")); @@ -65,13 +65,7 @@ void testPartialPathRouteWithSubpath() { assertEquals("/path/", ((TestRouting) result.getRight()).path); } - private static class TestRouting implements Routing { - private final String path; - - public TestRouting(String path) { - this.path = path; - } - + private record TestRouting(String path) implements Routing { @Override public ResponseData fetchData(RoutingPath subPath, HttpExchange request, ReportViewer viewer) { return null; diff --git a/endtoend-testing/src/main/java/de/jplag/endtoend/helper/LanguageDeserializer.java b/endtoend-testing/src/main/java/de/jplag/endtoend/helper/LanguageDeserializer.java index 07848b72e..22e231eac 100644 --- a/endtoend-testing/src/main/java/de/jplag/endtoend/helper/LanguageDeserializer.java +++ b/endtoend-testing/src/main/java/de/jplag/endtoend/helper/LanguageDeserializer.java @@ -3,7 +3,7 @@ import java.io.IOException; import de.jplag.Language; -import de.jplag.cli.LanguageLoader; +import de.jplag.cli.options.LanguageLoader; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext;