TokenPrinter.java 8.7 KB

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