JPlag.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. package de.jplag;
  2. import static de.jplag.options.Verbosity.LONG;
  3. import static de.jplag.options.Verbosity.QUIET;
  4. import java.io.BufferedReader;
  5. import java.io.File;
  6. import java.io.FileReader;
  7. import java.io.IOException;
  8. import java.lang.reflect.Constructor;
  9. import java.lang.reflect.InvocationTargetException;
  10. import java.util.ArrayList;
  11. import java.util.Arrays;
  12. import java.util.HashSet;
  13. import java.util.Iterator;
  14. import java.util.List;
  15. import java.util.Optional;
  16. import java.util.stream.Collectors;
  17. import de.jplag.options.JPlagOptions;
  18. import de.jplag.options.LanguageOption;
  19. import de.jplag.options.Verbosity;
  20. import de.jplag.strategy.ComparisonStrategy;
  21. import de.jplag.strategy.NormalComparisonStrategy;
  22. import de.jplag.strategy.ParallelComparisonStrategy;
  23. /**
  24. * This class coordinates the whole program flow.
  25. */
  26. public class JPlag implements Program {
  27. // INPUT:
  28. private HashSet<String> excludedFileNames = null; // Set of file names to be excluded in comparison.
  29. private Language language;
  30. // CORE COMPONENTS:
  31. private ComparisonStrategy comparisonStrategy;
  32. private GreedyStringTiling gSTiling = new GreedyStringTiling(this); // Contains the comparison logic.
  33. private final JPlagOptions options;
  34. // ERROR REPORTING:
  35. private String currentSubmissionName = "<Unknown submission>"; // TODO PB: This should be moved to parseSubmissions(...)
  36. private int errors = 0;
  37. private List<String> errorVector = new ArrayList<>(); // Vector of errors that occurred during the execution of the program.
  38. /**
  39. * Creates and initializes a JPlag instance, parameterized by a set of options.
  40. * @param options determines the parameterization.
  41. * @throws ExitException if the initialization fails.
  42. */
  43. public JPlag(JPlagOptions options) throws ExitException {
  44. this.options = options;
  45. initializeLanguage();
  46. initializeComparisonStrategy();
  47. checkBaseCodeOption();
  48. }
  49. /**
  50. * Main procedure, executes the comparison of source code submissions.
  51. * @return the results of the comparison, specifically the submissions whose similarity exceeds a set threshold.
  52. * @throws ExitException if the JPlag exits preemptively.
  53. */
  54. public JPlagResult run() throws ExitException {
  55. // 1. Preparation:
  56. File rootDir = new File(options.getRootDirectoryName());
  57. if (!rootDir.exists()) {
  58. throw new ExitException("Root directory " + options.getRootDirectoryName() + " does not exist!");
  59. }
  60. if (!rootDir.isDirectory()) {
  61. throw new ExitException(options.getRootDirectoryName() + " is not a directory!");
  62. }
  63. readExclusionFile(); // This file contains all files names which are excluded
  64. // 2. Parse and validate submissions:
  65. SubmissionSet submissionSet = findSubmissions(rootDir);
  66. parseAllSubmissions(submissionSet);
  67. submissionSet.setSubmissions(filterValidSubmissions(submissionSet.getSubmissions()));
  68. int submCount = submissionSet.numberOfSubmissions();
  69. if (submCount < 2) {
  70. printErrors();
  71. throw new ExitException("Not enough valid submissions! (found " + submCount + " valid submissions)",
  72. ExitException.NOT_ENOUGH_SUBMISSIONS_ERROR);
  73. }
  74. // 3. Compare valid submissions:
  75. errorVector = null; // errorVector is not needed anymore
  76. JPlagResult result = comparisonStrategy.compareSubmissions(submissionSet);
  77. System.out.println("Total time for comparing submissions: " + formatComparisonDuration(result.getDuration()));
  78. return result;
  79. }
  80. @Override
  81. public void addError(String errorMessage) {
  82. errorVector.add("[" + currentSubmissionName + "]\n" + errorMessage);
  83. print(errorMessage, null);
  84. }
  85. /**
  86. * @return the configured language in which the submissions are written.
  87. */
  88. public Language getLanguage() {
  89. return language;
  90. }
  91. /**
  92. * @return the program options which allow to configure JPlag.
  93. */
  94. protected JPlagOptions getOptions() {
  95. return this.options; // TODO TS: Should not be accessible, as options should be set before passing them to this class.
  96. }
  97. /**
  98. * Checks if a file has a valid suffix for the current language.
  99. * @param file is the file to check.
  100. * @return true if the file suffix matches the language.
  101. */
  102. public boolean hasValidSuffix(File file) {
  103. String[] validSuffixes = options.getFileSuffixes();
  104. // This is the case if either the language frontends or the CLI did not set the valid suffixes array in options
  105. if (validSuffixes == null || validSuffixes.length == 0) {
  106. return true;
  107. }
  108. return Arrays.stream(validSuffixes).anyMatch(suffix -> file.getName().endsWith(suffix));
  109. }
  110. /**
  111. * Checks if a file is excluded or not.
  112. */
  113. public boolean isFileExcluded(File file) {
  114. if (excludedFileNames == null) {
  115. return false;
  116. }
  117. return excludedFileNames.stream().anyMatch(excludedName -> file.getName().endsWith(excludedName));
  118. }
  119. @Override
  120. public void print(String message, String longMessage) {
  121. Verbosity verbosity = options.getVerbosity();
  122. if (verbosity != QUIET) {
  123. if (message != null) {
  124. System.out.print(message);
  125. }
  126. if (longMessage != null && verbosity == LONG) {
  127. System.out.print(longMessage);
  128. }
  129. }
  130. }
  131. /**
  132. * This method checks whether the base code directory value is valid.
  133. */
  134. private void checkBaseCodeOption() throws ExitException {
  135. if (options.hasBaseCode()) {
  136. if (!new File(options.getRootDirectoryName()).exists()) {
  137. throw new ExitException("Root directory \"" + options.getRootDirectoryName() + "\" doesn't exist!", ExitException.BAD_PARAMETER);
  138. }
  139. String baseCode = options.getBaseCodeSubmissionName().replace(File.separator, ""); // trim problematic file separators
  140. if (baseCode.contains(".")) {
  141. throw new ExitException("The basecode directory name \"" + baseCode + "\" cannot contain dots!", ExitException.BAD_PARAMETER);
  142. }
  143. String baseCodePath = options.getRootDirectoryName() + File.separator + baseCode;
  144. if (!new File(baseCodePath).exists()) {
  145. throw new ExitException("Basecode directory \"" + baseCodePath + "\" doesn't exist!", ExitException.BAD_PARAMETER);
  146. }
  147. String subdirectory = options.getSubdirectoryName();
  148. if (subdirectory != null && subdirectory.length() != 0) {
  149. if (!new File(baseCodePath, subdirectory).exists()) {
  150. throw new ExitException("Basecode directory doesn't contain" + " the subdirectory \"" + subdirectory + "\"!",
  151. ExitException.BAD_PARAMETER);
  152. }
  153. }
  154. options.setBaseCodeSubmissionName(baseCode);
  155. System.out.println("Basecode directory \"" + baseCodePath + "\" will be used");
  156. }
  157. }
  158. private List<Submission> filterValidSubmissions(List<Submission> submissions) {
  159. return submissions.stream().filter(submission -> !submission.hasErrors()).collect(Collectors.toCollection(ArrayList::new));
  160. }
  161. /**
  162. * Find all submissions in the given root directory.
  163. */
  164. private SubmissionSet findSubmissions(File rootDir) throws ExitException {
  165. String[] fileNamesInRootDir;
  166. try {
  167. fileNamesInRootDir = rootDir.list();
  168. } catch (SecurityException e) {
  169. throw new ExitException("Cannot list files of the root directory! " + e.getMessage());
  170. }
  171. if (fileNamesInRootDir == null) {
  172. throw new ExitException("Cannot list files of the root directory! " + "Make sure the specified root directory is in fact a directory.");
  173. }
  174. Arrays.sort(fileNamesInRootDir);
  175. return mapFileNamesInRootDirToSubmissions(fileNamesInRootDir, rootDir);
  176. }
  177. private void initializeComparisonStrategy() throws ExitException {
  178. switch (options.getComparisonMode()) {
  179. case NORMAL:
  180. comparisonStrategy = new NormalComparisonStrategy(options, gSTiling);
  181. break;
  182. case PARALLEL:
  183. comparisonStrategy = new ParallelComparisonStrategy(options, gSTiling);
  184. break;
  185. default:
  186. throw new ExitException("Illegal comparison mode: " + options.getComparisonMode());
  187. }
  188. }
  189. private void initializeLanguage() throws ExitException {
  190. LanguageOption languageOption = this.options.getLanguageOption();
  191. try {
  192. Constructor<?> constructor = Class.forName(languageOption.getClassPath()).getConstructor(Program.class);
  193. Object[] constructorParams = {this};
  194. Language language = (Language) constructor.newInstance(constructorParams);
  195. this.language = language;
  196. this.options.setLanguage(language);
  197. } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException
  198. | IllegalArgumentException | InvocationTargetException e) {
  199. e.printStackTrace();
  200. throw new ExitException("Language instantiation failed", ExitException.BAD_LANGUAGE_ERROR);
  201. }
  202. this.options.setLanguageDefaults(this.getLanguage());
  203. System.out.println("Initialized language " + this.getLanguage().getName());
  204. }
  205. private SubmissionSet mapFileNamesInRootDirToSubmissions(String[] fileNames, File rootDir) throws ExitException {
  206. List<Submission> submissions = new ArrayList<>();
  207. Optional<Submission> baseCodeSubmission = Optional.empty();
  208. for (String fileName : fileNames) {
  209. File submissionFile = new File(rootDir, fileName);
  210. if (isFileExcluded(submissionFile)) {
  211. System.out.println("Exclude submission: " + submissionFile.getName());
  212. continue;
  213. }
  214. if (submissionFile.isFile() && !hasValidSuffix(submissionFile)) {
  215. System.out.println("Ignore submission with invalid suffix: " + submissionFile.getName());
  216. continue;
  217. }
  218. if (submissionFile.isDirectory() && options.getSubdirectoryName() != null) {
  219. // Use subdirectory instead
  220. submissionFile = new File(submissionFile, options.getSubdirectoryName());
  221. if (!submissionFile.exists()) {
  222. throw new ExitException(
  223. String.format("Submission %s does not contain the given subdirectory '%s'", fileName, options.getSubdirectoryName()));
  224. }
  225. if (!submissionFile.isDirectory()) {
  226. throw new ExitException(String.format("The given subdirectory '%s' is not a directory!", options.getSubdirectoryName()));
  227. }
  228. }
  229. Submission submission = new Submission(fileName, submissionFile, this);
  230. if (options.hasBaseCode() && options.getBaseCodeSubmissionName().equals(fileName)) {
  231. baseCodeSubmission = Optional.of(submission);
  232. } else {
  233. submissions.add(submission);
  234. }
  235. }
  236. return new SubmissionSet(submissions, baseCodeSubmission);
  237. }
  238. /**
  239. * TODO PB: Find a better way to separate parseSubmissions(...) and parseBaseCodeSubmission(...)
  240. */
  241. private void parseAllSubmissions(SubmissionSet submissionSet) throws ExitException {
  242. try {
  243. parseSubmissions(submissionSet.getSubmissions());
  244. if (submissionSet.hasBaseCode()) {
  245. parseBaseCodeSubmission(submissionSet.getBaseCode());
  246. } else {
  247. // TODO:
  248. // options.useBasecode = false;
  249. }
  250. } catch (OutOfMemoryError e) {
  251. throw new ExitException("Out of memory during parsing of submission \"" + currentSubmissionName + "\"");
  252. } catch (Throwable e) {
  253. e.printStackTrace();
  254. throw new ExitException("Unknown exception during parsing of " + "submission \"" + currentSubmissionName + "\"");
  255. }
  256. }
  257. /**
  258. * Parse the given base code submission.
  259. */
  260. private void parseBaseCodeSubmission(Submission subm) throws ExitException {
  261. long startTime = System.currentTimeMillis();
  262. print("----- Parsing basecode submission: " + subm.getName() + "\n", null);
  263. // lets go:
  264. if (!subm.parse()) {
  265. printErrors();
  266. throw new ExitException("Bad basecode submission");
  267. }
  268. if (subm.getTokenList() != null && subm.getNumberOfTokens() < options.getMinimumTokenMatch()) {
  269. throw new ExitException("Basecode submission contains fewer tokens than minimum match length allows!\n");
  270. }
  271. if (options.hasBaseCode()) {
  272. gSTiling.createHashes(subm.getTokenList(), options.getMinimumTokenMatch(), true);
  273. }
  274. print("\nBasecode submission parsed!\n", null);
  275. long duration = System.currentTimeMillis() - startTime;
  276. print("\n", "\nTime for parsing Basecode: " + formatComparisonDuration(duration) + "\n");
  277. }
  278. /**
  279. * Parse all given submissions.
  280. */
  281. private void parseSubmissions(List<Submission> submissions) {
  282. if (submissions.isEmpty()) {
  283. System.out.println("No submissions to parse!");
  284. return;
  285. }
  286. int count = 0;
  287. long startTime = System.currentTimeMillis();
  288. Iterator<Submission> iter = submissions.iterator();
  289. int invalid = 0;
  290. while (iter.hasNext()) {
  291. boolean ok;
  292. boolean removed = false;
  293. Submission subm = iter.next();
  294. print(null, "------ Parsing submission: " + subm.getName() + "\n");
  295. currentSubmissionName = subm.getName();
  296. if (!(ok = subm.parse())) {
  297. errors++;
  298. }
  299. count++;
  300. if (subm.getTokenList() != null && subm.getNumberOfTokens() < options.getMinimumTokenMatch()) {
  301. print(null, "Submission contains fewer tokens than minimum match length allows!\n");
  302. subm.setTokenList(null);
  303. invalid++;
  304. removed = true;
  305. iter.remove();
  306. }
  307. if (ok && !removed) {
  308. print(null, "OK\n");
  309. } else {
  310. print(null, "ERROR -> Submission removed\n");
  311. }
  312. }
  313. print("\n" + (count - errors - invalid) + " submissions parsed successfully!\n" + errors + " parser error" + (errors != 1 ? "s!\n" : "!\n"),
  314. null);
  315. if (invalid != 0) {
  316. print(null,
  317. invalid + ((invalid == 1) ? " submission is not valid because it contains" : " submissions are not valid because they contain")
  318. + " fewer tokens than minimum match length allows.\n");
  319. }
  320. long duration = System.currentTimeMillis() - startTime;
  321. print("\n\n",
  322. "\nTotal time for parsing: " + formatComparisonDuration(duration) + "\n"
  323. + "Time per parsed submission: " + (count > 0 ? (duration / count) : "n/a") + " msec\n\n");
  324. }
  325. /**
  326. * Convert a duration in milli-seconds to a human-readable representation.
  327. * @param durationInMiliseconds Number of milli-seconds to convert.
  328. * @return Readable representation of the time interval.
  329. */
  330. private String formatComparisonDuration(long durationInMiliseconds) {
  331. int timeInSeconds = (int) (durationInMiliseconds / 1000);
  332. String hours = (timeInSeconds / 3600 > 0) ? (timeInSeconds / 3600) + " h " : "";
  333. String minutes = (timeInSeconds / 60 > 0) ? ((timeInSeconds / 60) % 60) + " min " : "";
  334. String seconds = (timeInSeconds % 60) + " sec";
  335. return hours + minutes + seconds;
  336. }
  337. /**
  338. * Print all errors from the errorVector.
  339. */
  340. private void printErrors() {
  341. StringBuilder errorStr = new StringBuilder();
  342. for (String str : errorVector) {
  343. errorStr.append(str);
  344. errorStr.append('\n');
  345. }
  346. System.out.println(errorStr.toString());
  347. }
  348. /*
  349. * If an exclusion file is given, it is read in and all stings are saved in the set "excluded".
  350. */
  351. private void readExclusionFile() {
  352. if (options.getExclusionFileName() == null) {
  353. return;
  354. }
  355. excludedFileNames = new HashSet<>();
  356. try {
  357. BufferedReader reader = new BufferedReader(new FileReader(options.getExclusionFileName(), JPlagOptions.CHARSET));
  358. String line;
  359. while ((line = reader.readLine()) != null) {
  360. excludedFileNames.add(line.trim());
  361. }
  362. reader.close();
  363. } catch (IOException exception) {
  364. System.out.println("Could not read exclusion file: " + exception.getMessage());
  365. }
  366. if (options.getVerbosity() == LONG) {
  367. print(null, "Excluded files:\n");
  368. for (String excludedFileName : excludedFileNames) {
  369. print(null, " " + excludedFileName + "\n");
  370. }
  371. }
  372. }
  373. }