TokenPrinter.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package de.jplag;
  2. import static java.util.stream.Collectors.toMap;
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.nio.file.Files;
  6. import java.nio.file.NoSuchFileException;
  7. import java.util.Collection;
  8. import java.util.Collections;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.Optional;
  12. import org.slf4j.Logger;
  13. import org.slf4j.LoggerFactory;
  14. /**
  15. * Utility class for printing JPlag tokens from a submission. Each line of code is printed starting with the line
  16. * number. Under these lines the tokens are annotated in the format <code>|TOKEN|</code>. The first vertical line marks
  17. * the token start, while the last vertical line marks the token end. Tokens that are shorter than the name do not end
  18. * with a vertical line, e.g. <code>|TOKEN</code>. Tokens with length 1 or 0 are printed in lower case, e.g.
  19. * <code>|token</code>.
  20. * @author Timur Saglam
  21. */
  22. public final class TokenPrinter {
  23. private static final Logger logger = LoggerFactory.getLogger(TokenPrinter.class);
  24. // Representation:
  25. private static final String BAR = "|";
  26. private static final String TAB = "\t";
  27. private static final String SPACE = " ";
  28. public static final String NON_WHITESPACE = "\\S";
  29. public static final int MIN_PADDING = 1;
  30. private static final int TAB_LENGTH = 8;
  31. private static final String TAB_REPLACEMENT = SPACE.repeat(TAB_LENGTH); // might depend on files
  32. // Configuration:
  33. private static final boolean INDICATE_TINY_TOKEN = true; // print token with length <= 1 in lowercase
  34. private static final boolean REPLACE_TABS = false;
  35. private TokenPrinter() {
  36. // Utility class, no public constructor.
  37. }
  38. /**
  39. * Creates a string representation of a set of files line by line and adds the tokens under the lines.
  40. * @param tokens is the set of tokens parsed from the files.
  41. * @param directory is the common directory of the files.
  42. * @param fileNames is a collection of the file names.
  43. * @param suffix is the optional view file suffix.
  44. * @return the string representation.
  45. */
  46. public static String printTokens(TokenList tokens, File directory, Collection<String> fileNames, Optional<String> suffix) {
  47. Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
  48. return printTokens(tokens, files, directory, suffix);
  49. }
  50. /**
  51. * Creates a string representation of a set of files line by line and adds the tokens under the lines.
  52. * @param tokens is the set of tokens parsed from the files.
  53. * @param directory is the common directory of the files.
  54. * @param fileNames is a collection of the file names.
  55. * @return the string representation.
  56. */
  57. public static String printTokens(TokenList tokens, File directory, Collection<String> fileNames) {
  58. Collection<File> files = fileNames.stream().map(name -> new File(directory, name)).toList();
  59. return printTokens(tokens, files, directory, Optional.empty());
  60. }
  61. /**
  62. * Creates a string representation of a collection of files line by line and adds the tokens under the lines.
  63. * @param tokens is the set of tokens parsed from the files.
  64. * @param files are the parsed files.
  65. * @return the string representation.
  66. */
  67. public static String printTokens(TokenList tokens, Collection<File> files, File root) {
  68. return printTokens(tokens, files, root, Optional.empty());
  69. }
  70. /**
  71. * Creates a string representation of a collection of files line by line and adds the tokens under the lines.
  72. * @param tokens is the set of tokens parsed from the files.
  73. * @param files are the parsed files.
  74. * @param suffix is the optional view file suffix.
  75. * @return the string representation.
  76. */
  77. public static String printTokens(TokenList tokens, Collection<File> files, File root, Optional<String> suffix) {
  78. Map<String, List<String>> filesToLines = readFiles(files);
  79. StringBuilder builder = new StringBuilder();
  80. int lineIndex = 0;
  81. int columnIndex = 1;
  82. String file = null; // no file yet
  83. String currentLine = null;
  84. for (Token token : tokens.allTokens()) {
  85. // New code file:
  86. if (!token.getFile().equals(file)) {
  87. if (file != null) {
  88. builder.append(System.lineSeparator());
  89. }
  90. file = token.getFile();
  91. builder.append(file);
  92. lineIndex = 0;
  93. }
  94. // New code line:
  95. if (token.getLine() > lineIndex) {
  96. lineIndex = token.getLine();
  97. columnIndex = 1;
  98. String fileName = token.getFile().isEmpty() ? root.getName() : token.getFile();
  99. fileName = fileName + suffix.orElse("");
  100. if (!filesToLines.containsKey(fileName)) {
  101. throw new IllegalStateException("Cannot find file " + fileName + " in " + filesToLines.keySet());
  102. }
  103. currentLine = filesToLines.get(fileName).get(lineIndex - 1);
  104. appendCodeLinePrefix(builder, lineIndex);
  105. appendCodeLine(builder, currentLine);
  106. appendTokenLinePrefix(builder, lineIndex);
  107. }
  108. // New line if already past token index:
  109. if (columnIndex > token.getColumn()) {
  110. appendTokenLinePrefix(builder, lineIndex);
  111. columnIndex = 1;
  112. }
  113. // Move to token index:
  114. columnIndex = appendPadding(builder, currentLine, columnIndex, token.getColumn());
  115. // Print the actual token:
  116. String stringRepresentation = getStringRepresentation(token);
  117. builder.append(BAR).append(stringRepresentation);
  118. columnIndex += stringRepresentation.length() + 1;
  119. // Move up to token end:
  120. int tokenEndIndex = token.getColumn() + token.getLength() - 1;
  121. columnIndex = appendPadding(builder, currentLine, columnIndex, tokenEndIndex);
  122. // Print token end if not already past it:
  123. if (columnIndex == tokenEndIndex) {
  124. builder.append(BAR);
  125. columnIndex++;
  126. }
  127. }
  128. return builder.toString();
  129. }
  130. /**
  131. * Appends the code line to the StringBuilder, applying the configuration of this TokenPrinter.
  132. * @param builder The StringBuilder to add code to
  133. * @param currentLine The line of code to append
  134. * @return a reference to that same StringBuilder
  135. */
  136. private static StringBuilder appendCodeLine(StringBuilder builder, String currentLine) {
  137. if (REPLACE_TABS) {
  138. currentLine = currentLine.replaceAll(TAB, TAB_REPLACEMENT);
  139. }
  140. return builder.append(currentLine);
  141. }
  142. /**
  143. * Appends whitespace padding to the given StringBuilder in order to reach the targetPosition. Note that <b>the indices
  144. * are 1-based</b>, whereas the positions in currentLine are 0-based. The convention that lines start at position 1
  145. * comes from the frontends, specifically the Java frontend.
  146. * @param builder The StringBuilder to add padding to
  147. * @param currentLine The current line in the code file, indicating where it containes tab characters
  148. * @param currentPosition The (1-based) index of the last character within the currentLine that is labeled, or 1.
  149. * @param targetPosition The (1-based) index of the next character within the currentLine that should be labeled
  150. * @return the new position, which is the targetPosition or the end of the line.
  151. */
  152. private static int appendPadding(StringBuilder builder, String currentLine, int currentPosition, int targetPosition) {
  153. targetPosition = Math.min(targetPosition, currentLine.length());
  154. if (currentPosition >= targetPosition) {
  155. return currentPosition;
  156. }
  157. // The replacement operation preserves TAB characters, which is essential for correct alignment
  158. String padding = currentLine.substring(currentPosition - 1, targetPosition - 1).replaceAll(TAB, REPLACE_TABS ? TAB_REPLACEMENT : TAB)
  159. .replaceAll(NON_WHITESPACE, SPACE);
  160. builder.append(padding);
  161. return targetPosition;
  162. }
  163. private static StringBuilder appendCodeLinePrefix(StringBuilder builder, int index) {
  164. builder.append(System.lineSeparator()).append(index);
  165. // Code line should be padded up to at least one tab length, and no less than MIN_PADDING
  166. int paddingLength = Math.max(TAB_LENGTH - digitCount(index), MIN_PADDING);
  167. return builder.append(SPACE.repeat(paddingLength));
  168. }
  169. private static StringBuilder appendTokenLinePrefix(StringBuilder builder, int index) {
  170. int paddingLength = Math.max(digitCount(index) + MIN_PADDING, TAB_LENGTH);
  171. String padding = SPACE.repeat(paddingLength);
  172. return builder.append(System.lineSeparator()).append(padding);
  173. }
  174. /**
  175. * Determines the string representation of the token.
  176. */
  177. private static String getStringRepresentation(Token token) {
  178. return token.getLength() <= 1 && INDICATE_TINY_TOKEN ? token.toString().toLowerCase() : token.toString();
  179. }
  180. /**
  181. * Turns a list of files into a map which maps the file names to a list of the contained lines of that file.
  182. */
  183. private static Map<String, List<String>> readFiles(Collection<File> allFiles) {
  184. return allFiles.stream().collect(toMap(File::getName, TokenPrinter::linesFromFile));
  185. }
  186. /**
  187. * Parses a file and returns a list of the contained lines.
  188. */
  189. private static List<String> linesFromFile(File file) {
  190. try {
  191. return Files.readAllLines(file.toPath());
  192. } catch (NoSuchFileException exception) {
  193. logger.error("File does not exist, thus no tokens are printed: " + file.getAbsolutePath());
  194. } catch (IOException exception) {
  195. logger.error("Cannot read " + file.getAbsolutePath() + ":", exception);
  196. }
  197. return Collections.emptyList();
  198. }
  199. /**
  200. * Returns the number of digits (including a minus) of the given number.
  201. */
  202. private static int digitCount(int number) {
  203. if (number == 0) {
  204. return 1;
  205. }
  206. int minusLength = number < 0 ? 1 : 0;
  207. // The 'log10' variant is supposedly faster than the 'toString' variant.
  208. return (int) Math.log10(Math.abs(number)) + minusLength + 1;
  209. }
  210. }