From 4d3d80ccedd305449b1298538c7b160ee64cb4db Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Thu, 22 Feb 2024 13:37:38 +0100 Subject: [PATCH 01/11] Rewritten the CollectedLogger --- .../de/jplag/cli/logger/CollectedLogger.java | 297 +++++------------- .../java/de/jplag/cli/logger/LogEntry.java | 15 + 2 files changed, 93 insertions(+), 219 deletions(-) create mode 100644 cli/src/main/java/de/jplag/cli/logger/LogEntry.java 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..44a69d867 100644 --- a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java +++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java @@ -1,308 +1,167 @@ package de.jplag.cli.logger; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.helpers.AbstractLogger; +import org.slf4j.helpers.MessageFormatter; + 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.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 - */ -public final class CollectedLogger extends MarkerIgnoringBase { - - @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; +public class CollectedLogger extends AbstractLogger { + private static final Level LOG_LEVEL_TRACE = Level.TRACE; + private static final Level LOG_LEVEL_DEBUG = Level.DEBUG; + private static final Level LOG_LEVEL_INFO = Level.INFO; + private static final Level LOG_LEVEL_WARN = Level.WARN; + private static final Level LOG_LEVEL_ERROR = Level.ERROR; /** * 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 Level LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = LOG_LEVEL_ERROR; - private static final int CURRENT_LOG_LEVEL = LOG_LEVEL_INFO; + private static final Level CURRENT_LOG_LEVEL = LOG_LEVEL_INFO; - /** - * The short name of this simple log instance - */ - private transient String shortLogName = null; + private static final int MAXIMUM_MESSAGE_LENGTH = 32; + + private static final PrintStream TARGET_STREAM = System.out; /** * Indicator whether finalization is in progress. + * * @see #printAllErrorsForLogger() */ private transient boolean isFinalizing = false; 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) { + public CollectedLogger(String name) { this.name = 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; - } - - StringBuilder builder = new StringBuilder(32); - - // Append date-time - builder.append(dateFormat.format(timeOfError == null ? new Date() : timeOfError)).append(' '); - - // Append current Level - builder.append('[').append(renderLevel(level)).append(']').append(' '); - - // Append the name of the log instance - if (shortLogName == null) { - shortLogName = computeShortName(); + allErrors.add(entry); + } else { + printLogEntry(entry); } - builder.append(shortLogName).append(" - "); - // Append the message - builder.append(message); - - write(builder, throwable); } - void printAllErrorsForLogger() { - this.isFinalizing = true; - // Copy errors to prevent infinite recursion - var errors = new ArrayList<>(this.allErrors); - if (errors.isEmpty()) { - return; + private String prepareFormattedMessage(String format, Object[] args) { + if (args == null) { + return format; } - this.allErrors.removeAll(errors); - - info("Summary of all Errors:"); - errors.forEach(error -> log(LOG_LEVEL_ERROR, error.first(), error.second(), error.third())); - isFinalizing = false; + return MessageFormatter.arrayFormat(format, args).getMessage(); } - @SuppressWarnings("java:S106") - void write(StringBuilder buf, Throwable throwable) { - PrintStream targetStream = System.out; - - targetStream.println(buf.toString()); - writeThrowable(throwable, targetStream); - targetStream.flush(); - } + private void printLogEntry(LogEntry entry) { + StringBuilder output = prepareLogOutput(entry); - private void writeThrowable(Throwable throwable, PrintStream targetStream) { - if (throwable != null) { - throwable.printStackTrace(targetStream); + TARGET_STREAM.println(output); + if (entry.cause() != null) { + entry.cause().printStackTrace(TARGET_STREAM); } + TARGET_STREAM.flush(); } - 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 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; } - private boolean isJPlagLog() { - return this.name.startsWith("de.jplag."); - } + void printAllErrorsForLogger() { + this.isFinalizing = true; + ArrayList errors = new ArrayList<>(this.allErrors); + + if(!errors.isEmpty()) { + info("Summary of all errors:"); + this.allErrors.removeAll(errors); + for (LogEntry errorEntry : errors) { + printLogEntry(errorEntry); + } + } - 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 + "]"); - }; + this.isFinalizing = false; } @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); + return isLogLevelEnabled(LOG_LEVEL_TRACE); } @Override - public void trace(String message, Throwable t) { - log(LOG_LEVEL_TRACE, message, t); + public boolean isTraceEnabled(Marker marker) { + return isTraceEnabled(); } @Override public boolean isDebugEnabled() { - return isLevelEnabled(LOG_LEVEL_DEBUG); + return isLogLevelEnabled(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); + public boolean isDebugEnabled(Marker marker) { + return isDebugEnabled(); } @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); + return isLogLevelEnabled(LOG_LEVEL_INFO); } @Override - public void info(String message, Throwable throwable) { - log(LOG_LEVEL_INFO, message, throwable); + public boolean isInfoEnabled(Marker marker) { + return isInfoEnabled(); } @Override public boolean isWarnEnabled() { - return isLevelEnabled(LOG_LEVEL_WARN); + return isLogLevelEnabled(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); + public boolean isWarnEnabled(Marker marker) { + return isWarnEnabled(); } @Override public boolean isErrorEnabled() { - return isLevelEnabled(LOG_LEVEL_ERROR); + return isLogLevelEnabled(LOG_LEVEL_ERROR); } @Override - public void error(String message) { - log(LOG_LEVEL_ERROR, message, null); + public boolean isErrorEnabled(Marker marker) { + return isErrorEnabled(); } - @Override - public void error(String format, Object arg) { - formatAndLog(LOG_LEVEL_ERROR, format, arg, null); + private boolean isLogLevelEnabled(Level logLevel) { + return logLevel.toInt() >= (isJPlagLog() ? CURRENT_LOG_LEVEL.toInt() : LOG_LEVEL_FOR_EXTERNAL_LIBRARIES.toInt()); } - @Override - public void error(String format, Object arg1, Object arg2) { - formatAndLog(LOG_LEVEL_ERROR, format, arg1, arg2); + private boolean isJPlagLog() { + return this.name.startsWith("de.jplag."); } - @Override - public void error(String format, Object... argArray) { - formatAndLog(LOG_LEVEL_ERROR, format, argArray); + private String computeShortName() { + return name.substring(name.lastIndexOf(".") + 1); } @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()); + 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..6b91c5af3 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/logger/LogEntry.java @@ -0,0 +1,15 @@ +package de.jplag.cli.logger; + +import org.slf4j.event.Level; + +import java.util.Date; + +/** + * 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) { +} From 6a8a4d1491380955643363c3fa7d129670d83cdf Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Thu, 22 Feb 2024 13:37:50 +0100 Subject: [PATCH 02/11] Various minor refactorings --- cli/src/main/java/de/jplag/cli/CLI.java | 7 +++++-- .../main/java/de/jplag/cli/OutputFileGenerator.java | 3 ++- .../java/de/jplag/cli/{ => options}/CliOptions.java | 4 +++- .../java/de/jplag/cli/{ => options}/JPlagMode.java | 2 +- .../de/jplag/cli/{ => options}/LanguageCandidates.java | 2 +- .../de/jplag/cli/{ => options}/LanguageConverter.java | 2 +- .../de/jplag/cli/{ => options}/LanguageLoader.java | 2 +- .../java/de/jplag/cli/{ => picocli}/CustomHelp.java | 2 +- .../java/de/jplag/cli/{ => picocli}/HelpFactory.java | 2 +- .../de/jplag/cli/{ => picocli}/ParamLabelRenderer.java | 2 +- .../main/java/de/jplag/cli/server/ReportViewer.java | 2 +- cli/src/main/java/de/jplag/cli/server/Routing.java | 4 ++-- cli/src/test/java/de/jplag/cli/ArgumentBuilder.java | 2 +- cli/src/test/java/de/jplag/cli/CustomHelpTests.java | 6 ++++-- cli/src/test/java/de/jplag/cli/LanguageTest.java | 2 ++ .../test/java/de/jplag/cli/ParamLabelRendererTest.java | 1 + .../test/java/de/jplag/cli/server/RoutingTreeTest.java | 10 ++-------- .../de/jplag/endtoend/helper/LanguageDeserializer.java | 2 +- 18 files changed, 31 insertions(+), 26 deletions(-) rename cli/src/main/java/de/jplag/cli/{ => options}/CliOptions.java (98%) rename cli/src/main/java/de/jplag/cli/{ => options}/JPlagMode.java (91%) rename cli/src/main/java/de/jplag/cli/{ => options}/LanguageCandidates.java (92%) rename cli/src/main/java/de/jplag/cli/{ => options}/LanguageConverter.java (91%) rename cli/src/main/java/de/jplag/cli/{ => options}/LanguageLoader.java (99%) rename cli/src/main/java/de/jplag/cli/{ => picocli}/CustomHelp.java (96%) rename cli/src/main/java/de/jplag/cli/{ => picocli}/HelpFactory.java (92%) rename cli/src/main/java/de/jplag/cli/{ => picocli}/ParamLabelRenderer.java (98%) diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index ac79e68c0..afb4ffbac 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -17,6 +17,9 @@ import java.util.Set; import java.util.stream.Collectors; +import de.jplag.cli.options.CliOptions; +import de.jplag.cli.options.LanguageLoader; +import de.jplag.cli.picocli.HelpFactory; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +69,7 @@ public final class CLI { 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"; + private static final String UNKNOWN_LANGUAGE_EXCEPTION = "Language %s does not exists. Available languages are: %s"; private static final String DESCRIPTION_PATTERN = "%nJPlag - %s%n%s%n%n"; @@ -180,7 +183,7 @@ public ParseResult parseOptions(String... args) throws CliException { 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(), + throw new CliException(String.format(UNKNOWN_LANGUAGE_EXCEPTION, e.getValue(), String.join(", ", LanguageLoader.getAllAvailableLanguageIdentifiers()))); } throw new CliException("Error during parsing", e); diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 028361346..5c42ff2df 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; +import de.jplag.cli.options.CliOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,7 +18,7 @@ private OutputFileGenerator() { } /** - * 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 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 98% 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..b1caeffbc 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; @@ -16,6 +16,7 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; +@SuppressWarnings({"CanBeFinal", "unused"}) @CommandLine.Command(name = "jplag", description = "", usageHelpAutoWidth = true, abbreviateSynopsis = true) public class CliOptions implements Runnable { public static final Language defaultLanguage = new JavaLanguage(); @@ -108,6 +109,7 @@ public static class Clustering { @ArgGroup public ClusteringEnabled enabled = new ClusteringEnabled(); + @SuppressWarnings("CanBeFinal") public static class ClusteringEnabled { @Option(names = {"--cluster-alg", "--cluster-algorithm"}, description = "Specifies the clustering algorithm (default: ${DEFAULT-VALUE}).") public ClusteringAlgorithm algorithm = new ClusteringOptions().algorithm(); 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/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/CustomHelpTests.java b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java index 3abde7505..ac3d37b86 100644 --- a/cli/src/test/java/de/jplag/cli/CustomHelpTests.java +++ b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java @@ -1,5 +1,8 @@ package de.jplag.cli; +import de.jplag.cli.picocli.CustomHelp; +import de.jplag.cli.picocli.HelpFactory; +import de.jplag.cli.picocli.ParamLabelRenderer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,7 +29,6 @@ void setup() { */ @Test void testReturnsCustomRenderer() { - Assertions.assertTrue(this.help.parameterLabelRenderer() instanceof ParamLabelRenderer, - "The custom help object returned the wrong ParamLabelRenderer type."); + 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..167faeae2 100644 --- a/cli/src/test/java/de/jplag/cli/LanguageTest.java +++ b/cli/src/test/java/de/jplag/cli/LanguageTest.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.List; +import de.jplag.cli.options.CliOptions; +import de.jplag.cli.options.LanguageLoader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java b/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java index f10a2d350..7348f9237 100644 --- a/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java +++ b/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java @@ -2,6 +2,7 @@ import java.util.List; +import de.jplag.cli.picocli.ParamLabelRenderer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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; From 3a76c2574036d146e7b7c3f5c9e3bde6c19ffb1e Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Fri, 23 Feb 2024 09:49:21 +0100 Subject: [PATCH 03/11] Split up the CLI class. --- cli/src/main/java/de/jplag/cli/CLI.java | 313 +++++------------- .../de/jplag/cli/JPlagOptionsBuilder.java | 97 ++++++ .../main/java/de/jplag/cli/JPlagRunner.java | 59 ++++ .../de/jplag/cli/OutputFileGenerator.java | 55 ++- .../de/jplag/cli/picocli/CliInputHandler.java | 168 ++++++++++ .../jplag/cli/CommandLineInterfaceTest.java | 8 +- 6 files changed, 444 insertions(+), 256 deletions(-) create mode 100644 cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java create mode 100644 cli/src/main/java/de/jplag/cli/JPlagRunner.java create mode 100644 cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index afb4ffbac..ad8aeda14 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -1,198 +1,120 @@ 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 de.jplag.cli.options.CliOptions; -import de.jplag.cli.options.LanguageLoader; -import de.jplag.cli.picocli.HelpFactory; -import org.slf4j.ILoggerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - 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 org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import picocli.CommandLine; -import picocli.CommandLine.Model.CommandSpec; -import picocli.CommandLine.Model.OptionSpec; -import picocli.CommandLine.ParseResult; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; /** * 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 JPlagRunner runner; + private final CliInputHandler inputHandler; + private final OutputFileGenerator outputFileGenerator; - private final CommandLine commandLine; - private final CliOptions options; + /** + * Creates a cli. + * + * @param runner The way to run JPlag + * @param args The command line arguments + */ + public CLI(JPlagRunner runner, OutputFileGenerator outputFileGenerator, String[] args) { + this.runner = runner; + this.outputFileGenerator = outputFileGenerator; + 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 UNKNOWN_LANGUAGE_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(); - - ParseResult parseResult = cli.parseOptions(args); + public boolean executeCliAndHandleErrors() { + boolean hadErrors = false; - 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); + public File runJPlag() throws ExitException, FileNotFoundException { + JPlagOptionsBuilder optionsBuilder = new JPlagOptionsBuilder(this.inputHandler); + JPlagOptions options = optionsBuilder.buildOptions(); + JPlagResult result = this.runner.runJPlag(options); - 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(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()); - - 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()); + this.outputFileGenerator.generateJPlagResultZip(result, target); + this.outputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.inputHandler.getCliOptions()); - 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(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); - } + public void runViewer(File zipFile) throws IOException { + this.runner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); } - private static void finalizeLogger() { + private void finalizeLogger() { ILoggerFactory factory = LoggerFactory.getILoggerFactory(); if (!(factory instanceof CollectedLoggerFactory collectedLoggerFactory)) { return; @@ -200,94 +122,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; } @@ -298,4 +134,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(JPlagRunner.DEFAULT_JPLAG_RUNNER, OutputFileGenerator.DEFAULT_OUTPUT_FILE_GENERATOR, 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..651a9b206 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java @@ -0,0 +1,97 @@ +package de.jplag.cli; + +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 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()); + + + ClusteringOptions clusteringOptions = getClusteringOptions(); + MergingOptions mergingOptions = getMergingOptions(); + + JPlagOptions jPlagOptions = 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); + + 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 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..ca2aa2eff --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -0,0 +1,59 @@ +package de.jplag.cli; + +import de.jplag.JPlag; +import de.jplag.JPlagResult; +import de.jplag.cli.server.ReportViewer; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.net.URI; + +/** + * Wraps the execution of the JPlag elements, so dummy implementations can be used for unit tests. + */ +public interface JPlagRunner { + Logger logger = LoggerFactory.getLogger(JPlagRunner.class); + + /** + * The default JPlag runner. Simply passes the calls to the appropriate JPlag elements + */ + JPlagRunner DEFAULT_JPLAG_RUNNER = new JPlagRunner() { + @Override + public JPlagResult runJPlag(JPlagOptions options) throws ExitException { + return JPlag.run(options); + } + + @Override + public 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(); + } + }; + + /** + * Executes JPlag + * @param options The options to pass to JPlag + * @return The result returned by JPlag + * @throws ExitException If JPlag throws an error + */ + JPlagResult runJPlag(JPlagOptions options) throws ExitException; + + /** + * 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 + */ + void runInternalServer(File zipFile, int port) throws IOException; +} diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 5c42ff2df..1790aa3a9 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -1,37 +1,58 @@ package de.jplag.cli; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import de.jplag.cli.options.CliOptions; +import de.jplag.reporting.reportobject.ReportObjectFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.jplag.JPlagResult; import de.jplag.csv.comparisons.CsvComparisonOutput; -public final class OutputFileGenerator { - private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); +public interface OutputFileGenerator { + OutputFileGenerator DEFAULT_OUTPUT_FILE_GENERATOR = new OutputFileGenerator() { + private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); - private OutputFileGenerator() { - // Prevents default constructor - } + @Override + public void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { + if (options.advanced.csvExport) { + try { + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), false, outputRoot, "results"); + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), true, outputRoot, "results-anonymous"); + } catch (IOException e) { + logger.warn("Could not write csv results", e); + } + } + } + + @Override + public 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/"); + } + }; /** * 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 result The result to export * @param outputRoot The root folder for the output - * @param options The cli options + * @param options The cli options */ - public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { - if (options.advanced.csvExport) { - try { - CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), false, outputRoot, "results"); - CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), true, outputRoot, "results-anonymous"); - } catch (IOException e) { - logger.warn("Could not write csv results", e); - } - } - } + void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options); + + /** + * Generates the JPLag result zip + * + * @param result The JPlag result + * @param outputFile The output file + * @throws FileNotFoundException If the file cannot be written + */ + void generateJPlagResultZip(JPlagResult result, File outputFile) throws FileNotFoundException; } 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..a878cab70 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java @@ -0,0 +1,168 @@ +package de.jplag.cli.picocli; + +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; + +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 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; + +/** + * 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 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(" -")) { + return " " + 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/test/java/de/jplag/cli/CommandLineInterfaceTest.java b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java index eb6ffca8c..7e8e2d783 100644 --- a/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java +++ b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java @@ -1,5 +1,6 @@ package de.jplag.cli; +import de.jplag.cli.picocli.CliInputHandler; import de.jplag.options.JPlagOptions; import picocli.CommandLine; @@ -14,7 +15,6 @@ public abstract class CommandLineInterfaceTest { protected static final String CURRENT_DIRECTORY = "."; protected static final double DELTA = 1E-5; - protected CLI cli; protected JPlagOptions options; /** @@ -36,9 +36,9 @@ protected ArgumentBuilder defaultArguments() { * @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(); } } From c339e1d2b1841019e277b3640be964a0fd609331 Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Fri, 23 Feb 2024 10:27:20 +0100 Subject: [PATCH 04/11] Split the CollectedLogger into two classes. --- .../de/jplag/cli/logger/CollectedLogger.java | 86 ++--------------- .../de/jplag/cli/logger/JPlagLoggerBase.java | 92 +++++++++++++++++++ 2 files changed, 98 insertions(+), 80 deletions(-) create mode 100644 cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java 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 44a69d867..9236c1470 100644 --- a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java +++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java @@ -2,7 +2,6 @@ import org.slf4j.Marker; import org.slf4j.event.Level; -import org.slf4j.helpers.AbstractLogger; import org.slf4j.helpers.MessageFormatter; import java.io.PrintStream; @@ -11,20 +10,10 @@ import java.util.Date; import java.util.concurrent.ConcurrentLinkedDeque; -public class CollectedLogger extends AbstractLogger { - private static final Level LOG_LEVEL_TRACE = Level.TRACE; - private static final Level LOG_LEVEL_DEBUG = Level.DEBUG; - private static final Level LOG_LEVEL_INFO = Level.INFO; - private static final Level LOG_LEVEL_WARN = Level.WARN; - private static final Level LOG_LEVEL_ERROR = Level.ERROR; - - /** - * The default log level that shall be used for external libraries (like Stanford Core NLP) - */ - private static final Level LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = LOG_LEVEL_ERROR; - - private static final Level CURRENT_LOG_LEVEL = LOG_LEVEL_INFO; - +/** + * A logger implementation, that prints all errors during finalization + */ +public class CollectedLogger extends JPlagLoggerBase { private static final int MAXIMUM_MESSAGE_LENGTH = 32; private static final PrintStream TARGET_STREAM = System.out; @@ -41,7 +30,7 @@ public class CollectedLogger extends AbstractLogger { private final ConcurrentLinkedDeque allErrors = new ConcurrentLinkedDeque<>(); public CollectedLogger(String name) { - this.name = name; + super(LOG_LEVEL_INFO, name); } @Override @@ -87,7 +76,7 @@ void printAllErrorsForLogger() { this.isFinalizing = true; ArrayList errors = new ArrayList<>(this.allErrors); - if(!errors.isEmpty()) { + if (!errors.isEmpty()) { info("Summary of all errors:"); this.allErrors.removeAll(errors); for (LogEntry errorEntry : errors) { @@ -98,70 +87,7 @@ void printAllErrorsForLogger() { this.isFinalizing = false; } - @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() ? CURRENT_LOG_LEVEL.toInt() : LOG_LEVEL_FOR_EXTERNAL_LIBRARIES.toInt()); - } - - private boolean isJPlagLog() { - return this.name.startsWith("de.jplag."); - } - private String computeShortName() { return name.substring(name.lastIndexOf(".") + 1); } - - @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/JPlagLoggerBase.java b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java new file mode 100644 index 000000000..fb63bdf19 --- /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 Level currentLogLevel; + + /** + * @param currentLogLevel The current log level + * @param name The name of the logger + */ + public 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 + } +} From 791ebfa3d642406be2ef39640a9dbfb3ecb4ae4c Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Fri, 23 Feb 2024 10:28:01 +0100 Subject: [PATCH 05/11] Split the CollectedLogger into two classes. --- cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java | 2 +- cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java index fb63bdf19..5ca6d5da9 100644 --- a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java +++ b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java @@ -16,7 +16,7 @@ public abstract class JPlagLoggerBase extends AbstractLogger { private static final Level LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = LOG_LEVEL_ERROR; - private Level currentLogLevel; + private final Level currentLogLevel; /** * @param currentLogLevel The current log level diff --git a/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java index 7e8e2d783..3946dbe45 100644 --- a/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java +++ b/cli/src/test/java/de/jplag/cli/CommandLineInterfaceTest.java @@ -3,8 +3,6 @@ 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 @@ -32,7 +30,7 @@ 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 { From c78a13af593947bfd8371d63b51ec1783995fba0 Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Fri, 23 Feb 2024 10:34:28 +0100 Subject: [PATCH 06/11] Simple refactorings for the cli module --- cli/src/main/java/de/jplag/cli/CLI.java | 28 ++++++------ .../de/jplag/cli/JPlagOptionsBuilder.java | 43 +++++++++++-------- .../main/java/de/jplag/cli/JPlagRunner.java | 15 ++++--- .../de/jplag/cli/OutputFileGenerator.java | 12 +++--- .../de/jplag/cli/logger/CollectedLogger.java | 9 ++-- .../de/jplag/cli/logger/JPlagLoggerBase.java | 2 +- .../java/de/jplag/cli/logger/LogEntry.java | 4 +- .../main/java/de/jplag/cli/logger/Triple.java | 4 -- .../de/jplag/cli/picocli/CliInputHandler.java | 35 ++++++++------- .../java/de/jplag/cli/CustomHelpTests.java | 10 +++-- .../test/java/de/jplag/cli/LanguageTest.java | 4 +- .../de/jplag/cli/ParamLabelRendererTest.java | 3 +- 12 files changed, 84 insertions(+), 85 deletions(-) delete mode 100644 cli/src/main/java/de/jplag/cli/logger/Triple.java diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index ad8aeda14..f5184bc83 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -1,5 +1,13 @@ package de.jplag.cli; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.slf4j.ILoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import de.jplag.JPlag; import de.jplag.JPlagResult; import de.jplag.cli.logger.CollectedLoggerFactory; @@ -8,17 +16,9 @@ import de.jplag.exceptions.ExitException; import de.jplag.logging.ProgressBarLogger; import de.jplag.options.JPlagOptions; -import org.slf4j.ILoggerFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; /** * Command line interface class, allows using via command line. - * * @see CLI#main(String[]) */ public final class CLI { @@ -32,9 +32,8 @@ public final class CLI { /** * Creates a cli. - * * @param runner The way to run JPlag - * @param args The command line arguments + * @param args The command line arguments */ public CLI(JPlagRunner runner, OutputFileGenerator outputFileGenerator, String[] args) { this.runner = runner; @@ -44,9 +43,8 @@ public CLI(JPlagRunner runner, OutputFileGenerator outputFileGenerator, String[] /** * Executes the cli - * * @throws ExitException If anything on the side of JPlag goes wrong - * @throws IOException If any files did not work + * @throws IOException If any files did not work */ public void executeCli() throws ExitException, IOException { logger.debug("Your version of JPlag is {}", JPlag.JPLAG_VERSION); @@ -64,7 +62,6 @@ public void executeCli() throws ExitException, IOException { /** * Executes the cli and handles the exceptions that might occur. - * * @return true, if an exception has been caught. */ public boolean executeCliAndHandleErrors() { @@ -88,9 +85,8 @@ public boolean executeCliAndHandleErrors() { /** * 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 ExitException If JPlag threw an exception * @throws FileNotFoundException If the file could not be written */ public File runJPlag() throws ExitException, FileNotFoundException { @@ -111,7 +107,7 @@ public File runJPlag() throws ExitException, FileNotFoundException { * @throws IOException If something went wrong with the internal server */ public void runViewer(File zipFile) throws IOException { - this.runner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); + this.runner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); } private void finalizeLogger() { diff --git a/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java b/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java index 651a9b206..16c309f4b 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java +++ b/cli/src/main/java/de/jplag/cli/JPlagOptionsBuilder.java @@ -1,18 +1,19 @@ 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.HashSet; -import java.util.List; -import java.util.Set; /** * Handles the building of JPlag options from the cli options @@ -43,14 +44,7 @@ public JPlagOptions buildOptions() throws CliException { submissionDirectories.addAll(List.of(this.cliOptions.newDirectories)); submissionDirectories.addAll(this.cliInputHandler.getSubcommandSubmissionDirectories()); - - ClusteringOptions clusteringOptions = getClusteringOptions(); - MergingOptions mergingOptions = getMergingOptions(); - - JPlagOptions jPlagOptions = 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); + JPlagOptions jPlagOptions = initializeJPlagOptions(submissionDirectories, oldSubmissionDirectories, suffixes); String baseCodePath = this.cliOptions.baseCode; File baseCodeDirectory = baseCodePath == null ? null : new File(baseCodePath); @@ -61,11 +55,23 @@ public JPlagOptions buildOptions() throws CliException { 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) + .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); @@ -92,6 +98,7 @@ private ClusteringOptions getClusteringOptions() { } private MergingOptions getMergingOptions() { - return new MergingOptions(this.cliOptions.merging.enabled, this.cliOptions.merging.minimumNeighborLength, this.cliOptions.merging.maximumGapSize); + 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 index ca2aa2eff..8a69bd7a2 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagRunner.java +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -1,17 +1,18 @@ 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; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.awt.Desktop; -import java.io.File; -import java.io.IOException; -import java.net.URI; /** * Wraps the execution of the JPlag elements, so dummy implementations can be used for unit tests. diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 1790aa3a9..90248ed6f 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -4,13 +4,13 @@ import java.io.FileNotFoundException; import java.io.IOException; -import de.jplag.cli.options.CliOptions; -import de.jplag.reporting.reportobject.ReportObjectFactory; 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; public interface OutputFileGenerator { OutputFileGenerator DEFAULT_OUTPUT_FILE_GENERATOR = new OutputFileGenerator() { @@ -40,17 +40,15 @@ public void generateJPlagResultZip(JPlagResult result, File outputFile) throws F /** * 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 result The result to export * @param outputRoot The root folder for the output - * @param options The cli options + * @param options The cli options */ void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options); /** * Generates the JPLag result zip - * - * @param result The JPlag result + * @param result The JPlag result * @param outputFile The output file * @throws FileNotFoundException If the file cannot be written */ 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 9236c1470..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,15 +1,15 @@ package de.jplag.cli.logger; -import org.slf4j.Marker; -import org.slf4j.event.Level; -import org.slf4j.helpers.MessageFormatter; - import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.ConcurrentLinkedDeque; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.helpers.MessageFormatter; + /** * A logger implementation, that prints all errors during finalization */ @@ -20,7 +20,6 @@ public class CollectedLogger extends JPlagLoggerBase { /** * Indicator whether finalization is in progress. - * * @see #printAllErrorsForLogger() */ private transient boolean isFinalizing = false; diff --git a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java index 5ca6d5da9..687426e8a 100644 --- a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java +++ b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java @@ -87,6 +87,6 @@ private boolean isJPlagLog() { @Override protected String getFullyQualifiedCallerName() { - return null; //does not seem to be used by anything, but is required by SLF4J + 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 index 6b91c5af3..86d94d2ce 100644 --- a/cli/src/main/java/de/jplag/cli/logger/LogEntry.java +++ b/cli/src/main/java/de/jplag/cli/logger/LogEntry.java @@ -1,9 +1,9 @@ package de.jplag.cli.logger; -import org.slf4j.event.Level; - import java.util.Date; +import org.slf4j.event.Level; + /** * Holds a log entry for later usage * @param message The message of the log 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/picocli/CliInputHandler.java b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java index a878cab70..c27e91faf 100644 --- a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java +++ b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java @@ -1,13 +1,8 @@ package de.jplag.cli.picocli; -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; +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; @@ -17,9 +12,15 @@ import java.util.Random; import java.util.stream.Collectors; -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 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 @@ -94,19 +95,20 @@ private List buildSubcommands() { /** * 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())) { + 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")) { + 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()))); } @@ -119,7 +121,6 @@ public boolean parse() throws CliException { /** * 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() { @@ -138,8 +139,7 @@ public Language getSelectedLanguage() throws CliException { ParseResult subcommand = this.parseResult.subcommand(); - Language language = LanguageLoader.getLanguage(subcommand.commandSpec().name()) - .orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION)); + Language language = LanguageLoader.getLanguage(subcommand.commandSpec().name()).orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION)); LanguageOptions languageOptions = language.getOptions(); languageOptions.getOptionsAsList().forEach(option -> { @@ -165,4 +165,3 @@ private String generateDescription() { return String.format(DESCRIPTION_PATTERN, randomDescription, CREDITS); } } - diff --git a/cli/src/test/java/de/jplag/cli/CustomHelpTests.java b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java index ac3d37b86..9fecd3f3b 100644 --- a/cli/src/test/java/de/jplag/cli/CustomHelpTests.java +++ b/cli/src/test/java/de/jplag/cli/CustomHelpTests.java @@ -1,12 +1,13 @@ package de.jplag.cli; -import de.jplag.cli.picocli.CustomHelp; -import de.jplag.cli.picocli.HelpFactory; -import de.jplag.cli.picocli.ParamLabelRenderer; import org.junit.jupiter.api.Assertions; 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; /** @@ -29,6 +30,7 @@ void setup() { */ @Test void testReturnsCustomRenderer() { - Assertions.assertInstanceOf(ParamLabelRenderer.class, this.help.parameterLabelRenderer(), "The custom help object returned the wrong ParamLabelRenderer type."); + 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 167faeae2..a23911284 100644 --- a/cli/src/test/java/de/jplag/cli/LanguageTest.java +++ b/cli/src/test/java/de/jplag/cli/LanguageTest.java @@ -5,12 +5,12 @@ import java.util.Arrays; import java.util.List; -import de.jplag.cli.options.CliOptions; -import de.jplag.cli.options.LanguageLoader; import org.junit.jupiter.api.Assertions; 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 7348f9237..817b84fef 100644 --- a/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java +++ b/cli/src/test/java/de/jplag/cli/ParamLabelRendererTest.java @@ -2,13 +2,14 @@ import java.util.List; -import de.jplag.cli.picocli.ParamLabelRenderer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import de.jplag.cli.picocli.ParamLabelRenderer; + import picocli.CommandLine; /** From 0ab8cd99b3f6d66f18c3d2d8d34fdae74c6f3249 Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Fri, 23 Feb 2024 11:11:41 +0100 Subject: [PATCH 07/11] Sonarcloud issues --- cli/src/main/java/de/jplag/cli/OutputFileGenerator.java | 4 ++-- cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 90248ed6f..15d5cd5aa 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -13,9 +13,9 @@ import de.jplag.reporting.reportobject.ReportObjectFactory; public interface OutputFileGenerator { - OutputFileGenerator DEFAULT_OUTPUT_FILE_GENERATOR = new OutputFileGenerator() { - private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); + Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); + OutputFileGenerator DEFAULT_OUTPUT_FILE_GENERATOR = new OutputFileGenerator() { @Override public void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { if (options.advanced.csvExport) { diff --git a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java index 687426e8a..34a59fedc 100644 --- a/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java +++ b/cli/src/main/java/de/jplag/cli/logger/JPlagLoggerBase.java @@ -22,7 +22,7 @@ public abstract class JPlagLoggerBase extends AbstractLogger { * @param currentLogLevel The current log level * @param name The name of the logger */ - public JPlagLoggerBase(Level currentLogLevel, String name) { + protected JPlagLoggerBase(Level currentLogLevel, String name) { this.currentLogLevel = currentLogLevel; this.name = name; } From f8bb719ee53a1861f3550431bc6a9330d37da3ba Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Sun, 3 Mar 2024 20:08:54 +0100 Subject: [PATCH 08/11] Removed suppress warnings. --- cli/src/main/java/de/jplag/cli/options/CliOptions.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/options/CliOptions.java b/cli/src/main/java/de/jplag/cli/options/CliOptions.java index b1caeffbc..999cd5635 100644 --- a/cli/src/main/java/de/jplag/cli/options/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/options/CliOptions.java @@ -16,7 +16,6 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; -@SuppressWarnings({"CanBeFinal", "unused"}) @CommandLine.Command(name = "jplag", description = "", usageHelpAutoWidth = true, abbreviateSynopsis = true) public class CliOptions implements Runnable { public static final Language defaultLanguage = new JavaLanguage(); @@ -109,7 +108,6 @@ public static class Clustering { @ArgGroup public ClusteringEnabled enabled = new ClusteringEnabled(); - @SuppressWarnings("CanBeFinal") public static class ClusteringEnabled { @Option(names = {"--cluster-alg", "--cluster-algorithm"}, description = "Specifies the clustering algorithm (default: ${DEFAULT-VALUE}).") public ClusteringAlgorithm algorithm = new ClusteringOptions().algorithm(); From a9e6ef55b9bd38b35af8c8ee2b04351a3c0f93c2 Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Tue, 2 Apr 2024 10:26:06 +0200 Subject: [PATCH 09/11] Removed superfluous interfaces --- cli/src/main/java/de/jplag/cli/CLI.java | 17 ++---- .../main/java/de/jplag/cli/JPlagRunner.java | 48 +++++++--------- .../de/jplag/cli/OutputFileGenerator.java | 56 +++++++++---------- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index f5184bc83..e64b81118 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -26,18 +26,13 @@ public final class CLI { private static final String DEFAULT_FILE_ENDING = ".zip"; - private final JPlagRunner runner; private final CliInputHandler inputHandler; - private final OutputFileGenerator outputFileGenerator; /** * Creates a cli. - * @param runner The way to run JPlag * @param args The command line arguments */ - public CLI(JPlagRunner runner, OutputFileGenerator outputFileGenerator, String[] args) { - this.runner = runner; - this.outputFileGenerator = outputFileGenerator; + public CLI(String[] args) { this.inputHandler = new CliInputHandler(args); } @@ -92,11 +87,11 @@ public boolean executeCliAndHandleErrors() { public File runJPlag() throws ExitException, FileNotFoundException { JPlagOptionsBuilder optionsBuilder = new JPlagOptionsBuilder(this.inputHandler); JPlagOptions options = optionsBuilder.buildOptions(); - JPlagResult result = this.runner.runJPlag(options); + JPlagResult result = JPlagRunner.runJPlag(options); File target = new File(getResultFilePath()); - this.outputFileGenerator.generateJPlagResultZip(result, target); - this.outputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.inputHandler.getCliOptions()); + OutputFileGenerator.generateJPlagResultZip(result, target); + OutputFileGenerator.generateCsvOutput(result, new File(getResultFileBaseName()), this.inputHandler.getCliOptions()); return target; } @@ -107,7 +102,7 @@ public File runJPlag() throws ExitException, FileNotFoundException { * @throws IOException If something went wrong with the internal server */ public void runViewer(File zipFile) throws IOException { - this.runner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); + JPlagRunner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); } private void finalizeLogger() { @@ -132,7 +127,7 @@ private String getResultFileBaseName() { } public static void main(String[] args) { - CLI cli = new CLI(JPlagRunner.DEFAULT_JPLAG_RUNNER, OutputFileGenerator.DEFAULT_OUTPUT_FILE_GENERATOR, args); + CLI cli = new CLI(args); if (cli.executeCliAndHandleErrors()) { System.exit(1); } diff --git a/cli/src/main/java/de/jplag/cli/JPlagRunner.java b/cli/src/main/java/de/jplag/cli/JPlagRunner.java index 8a69bd7a2..7df62858b 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagRunner.java +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -15,46 +15,40 @@ import de.jplag.options.JPlagOptions; /** - * Wraps the execution of the JPlag elements, so dummy implementations can be used for unit tests. + * Wraps the execution of the JPlag components */ -public interface JPlagRunner { - Logger logger = LoggerFactory.getLogger(JPlagRunner.class); +public final class JPlagRunner { + private static final Logger logger = LoggerFactory.getLogger(JPlagRunner.class); - /** - * The default JPlag runner. Simply passes the calls to the appropriate JPlag elements - */ - JPlagRunner DEFAULT_JPLAG_RUNNER = new JPlagRunner() { - @Override - public JPlagResult runJPlag(JPlagOptions options) throws ExitException { - return JPlag.run(options); - } - - @Override - public 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(); - } - }; + 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 */ - JPlagResult runJPlag(JPlagOptions options) throws ExitException; + 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 + * @param port The port to open the server on * @throws IOException If the internal server throws an exception */ - void runInternalServer(File zipFile, int port) throws IOException; + 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 15d5cd5aa..333bcc428 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -12,45 +12,45 @@ import de.jplag.csv.comparisons.CsvComparisonOutput; import de.jplag.reporting.reportobject.ReportObjectFactory; -public interface OutputFileGenerator { - Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); +/** + * Manages the creation of output files + */ +public final class OutputFileGenerator { + private static final Logger logger = LoggerFactory.getLogger(OutputFileGenerator.class); - OutputFileGenerator DEFAULT_OUTPUT_FILE_GENERATOR = new OutputFileGenerator() { - @Override - public void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { - if (options.advanced.csvExport) { - try { - CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), false, outputRoot, "results"); - CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), true, outputRoot, "results-anonymous"); - } catch (IOException e) { - logger.warn("Could not write csv results", e); - } - } - } - - @Override - public 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/"); - } - }; + private OutputFileGenerator() { + } /** * 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 result The result to export * @param outputRoot The root folder for the output - * @param options The cli options + * @param options The cli options */ - void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options); + public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { + if (options.advanced.csvExport) { + try { + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), false, outputRoot, "results"); + CsvComparisonOutput.writeCsvResults(result.getAllComparisons(), true, outputRoot, "results-anonymous"); + } catch (IOException e) { + logger.warn("Could not write csv results", e); + } + } + } /** * Generates the JPLag result zip - * @param result The JPlag result + * + * @param result The JPlag result * @param outputFile The output file * @throws FileNotFoundException If the file cannot be written */ - void generateJPlagResultZip(JPlagResult result, File outputFile) throws FileNotFoundException; + 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/"); + } } From e78d6800fb93874461bbee11fbfc47f6019e474b Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Tue, 2 Apr 2024 10:57:05 +0200 Subject: [PATCH 10/11] Spotless --- cli/src/main/java/de/jplag/cli/JPlagRunner.java | 4 +--- cli/src/main/java/de/jplag/cli/OutputFileGenerator.java | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/JPlagRunner.java b/cli/src/main/java/de/jplag/cli/JPlagRunner.java index 7df62858b..e53a0d03d 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagRunner.java +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -25,7 +25,6 @@ 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 @@ -36,9 +35,8 @@ public static JPlagResult runJPlag(JPlagOptions options) throws ExitException { /** * 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 + * @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 { diff --git a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java index 333bcc428..4d9d41c84 100644 --- a/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java +++ b/cli/src/main/java/de/jplag/cli/OutputFileGenerator.java @@ -24,10 +24,9 @@ private OutputFileGenerator() { /** * 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 result The result to export * @param outputRoot The root folder for the output - * @param options The cli options + * @param options The cli options */ public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOptions options) { if (options.advanced.csvExport) { @@ -42,8 +41,7 @@ public static void generateCsvOutput(JPlagResult result, File outputRoot, CliOpt /** * Generates the JPLag result zip - * - * @param result The JPlag result + * @param result The JPlag result * @param outputFile The output file * @throws FileNotFoundException If the file cannot be written */ From 8a337ddbdf9196d19a633173d9540c27a79855d2 Mon Sep 17 00:00:00 2001 From: Alexander Milster Date: Tue, 9 Apr 2024 15:23:36 +0200 Subject: [PATCH 11/11] Removed magic strings. --- .../main/java/de/jplag/cli/picocli/CliInputHandler.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java index c27e91faf..2062b6380 100644 --- a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java +++ b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java @@ -39,6 +39,9 @@ public class CliInputHandler { 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; @@ -62,8 +65,8 @@ private CommandLine buildCommandLine() { cli.setHelpFactory(new HelpFactory()); cli.getHelpSectionMap().put(SECTION_KEY_OPTION_LIST, help -> help.optionList().lines().map(it -> { - if (it.startsWith(" -")) { - return " " + it; + if (it.startsWith(PARAMETER_SHORT_PREFIX)) { + return PARAMETER_SHORT_ADDITIONAL_INDENT + it; } return it; }).collect(Collectors.joining(System.lineSeparator())));