package de.jplag; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents a single submission. A submission can contain multiple files. */ public class Submission implements Comparable { private static final Logger logger = LoggerFactory.getLogger(Submission.class); /** * Directory name for storing submission files with parse errors if so requested. */ private static final String ERROR_FOLDER = "errors"; /** * Identification of the submission (often a directory or file name). */ private final String name; /** * Root of the submission files (including the subdir if used). */ private final File submissionRootFile; /** * Whether the submission is new. That is, must be checked for plagiarism. */ private final boolean isNew; /** * Files of the submission. */ private final Collection files; /** * Whether an error occurred during parsing the submission files. */ private boolean hasErrors; /** * Parse result, tokens from all files. */ private List tokenList; /** * Base code comparison */ private JPlagComparison baseCodeComparison; private final Language language; /** * Creates a submission. * @param name Identification of the submission (directory or filename). * @param submissionRootFile is the submission file, or the root of the submission itself. * @param isNew states whether the submission must be checked for plagiarism. * @param files are the files of the submissions, if the root is a single file it should just contain one file. * @param language is the language of the submission. */ public Submission(String name, File submissionRootFile, boolean isNew, Collection files, Language language) { this.name = name; this.submissionRootFile = submissionRootFile; this.isNew = isNew; this.files = files; this.language = language; } @Override public int compareTo(Submission other) { return name.compareTo(other.name); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof Submission otherSubmission)) { return false; } return otherSubmission.getName().equals(name); } @Override public int hashCode() { return Objects.hash(name); } /** * @return base code comparison */ public JPlagComparison getBaseCodeComparison() { return baseCodeComparison; } /** * @return a list of files this submission consists of. */ public Collection getFiles() { return files; } /** * @return name of the submission (directory or file name). */ public String getName() { return name; } /** * @return Number of tokens in the parse result. */ public int getNumberOfTokens() { if (tokenList == null) { return 0; } return tokenList.size(); } /** * @return the unique file of the submission, which is either in a root folder or a subfolder of root folder when the * subdirectory option is used. */ public File getRoot() { return submissionRootFile; } /** * @param subtractBaseCode If true subtract basecode matches if possible. * @return Similarity divisor for the submission. */ int getSimilarityDivisor(boolean subtractBaseCode) { int divisor = getNumberOfTokens() - getFiles().size(); if (subtractBaseCode && baseCodeComparison != null) { divisor -= baseCodeComparison.getNumberOfMatchedTokens(); } return divisor; } /** * @return unmodifiable list of tokens generated by parsing the submission. */ public List getTokenList() { return tokenList == null ? null : Collections.unmodifiableList(tokenList); } /** * @return Whether a comparison between the submission and the base code is available. */ public boolean hasBaseCodeMatches() { return baseCodeComparison != null; } /** * @return true if at least one error occurred while parsing this submission; false otherwise. */ public boolean hasErrors() { return hasErrors; } /** * @return whether the submission is new, That is, must be checked for plagiarism. */ public boolean isNew() { return isNew; } /** * Sets the base code comparison * @param baseCodeComparison is submissions matches with the base code */ public void setBaseCodeComparison(JPlagComparison baseCodeComparison) { this.baseCodeComparison = baseCodeComparison; } /** * Sets the tokens that have been parsed from the files this submission consists of. * @param tokenList is the list of these tokens. */ public void setTokenList(List tokenList) { this.tokenList = tokenList; } /** * String representation of the code files contained in this submission, annotated with all tokens. * @return the annotated code as string. */ public String getTokenAnnotatedSourceCode() { return TokenPrinter.printTokens(tokenList, submissionRootFile); } @Override public String toString() { return name; } /** * This method is used to copy files that can not be parsed to a special folder. */ private void copySubmission() { File rootDirectory = submissionRootFile.getParentFile(); assert rootDirectory != null; File submissionDirectory = createSubdirectory(rootDirectory, ERROR_FOLDER, language.getIdentifier(), name); for (File file : files) { try { Files.copy(file.toPath(), new File(submissionDirectory, file.getName()).toPath()); } catch (IOException exception) { logger.error("Error copying file: " + exception.getMessage(), exception); } } } private static File createSubdirectory(File parent, String... subdirectoryNames) { File subdirectory = parent; for (String name : subdirectoryNames) { subdirectory = new File(subdirectory, name); } if (!subdirectory.exists()) { subdirectory.mkdirs(); } return subdirectory; } /* package-private */ void markAsErroneous() { hasErrors = true; } /** * Parse files of the submission. * @return Whether parsing was successful. */ /* package-private */ boolean parse(boolean debugParser) { if (files == null || files.isEmpty()) { logger.error("ERROR: nothing to parse for submission \"{}\"", name); tokenList = null; hasErrors = true; // invalidate submission return false; } try { tokenList = language.parse(new HashSet<>(files)); } catch (ParsingException e) { logger.warn("Failed to parse submission {} with error {}", this, e); tokenList = null; hasErrors = true; if (debugParser) { copySubmission(); } return false; } if (tokenList.size() < 3) { logger.error("Submission \"{}\" is too short!", name); tokenList = null; hasErrors = true; // invalidate submission return false; } return true; } }