edu.hm.hafner.analysis.FullTextFingerprint Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of analysis-model Show documentation
Show all versions of analysis-model Show documentation
This library provides a Java object model to read, aggregate, filter, and query static analysis reports.
It is used by Jenkins' warnings next generation plug-in to visualize the warnings of individual builds.
Additionally, this library is used by a GitHub action to autograde student software projects based on a given set of
metrics (unit tests, code and mutation coverage, static analysis warnings).
package edu.hm.hafner.analysis;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Locale;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import com.google.errorprone.annotations.MustBeClosed;
import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Creates a fingerprint of the specified issue using the source code at the affected line. The fingerprint is computed
* using the 1:1 content of a small number of lines before and after the affected line (see {@link #LINES_LOOK_AHEAD}).
*
* @author Ullrich Hafner
*/
public class FullTextFingerprint {
/** Number of lines before and after current line to consider. */
private static final int LINES_LOOK_AHEAD = 3;
private static final int LINE_RANGE_BUFFER_SIZE = 1000;
private static final char[] HEX_CHARACTERS = "0123456789ABCDEF".toCharArray();
@SuppressWarnings("PMD.AvoidMessageDigestField")
private final MessageDigest digest;
private final FileSystem fileSystem;
/**
* Creates a new instance of {@link FullTextFingerprint}.
*/
public FullTextFingerprint() {
this(new FileSystem());
}
@VisibleForTesting
@SuppressFBWarnings(value = "WEAK_MESSAGE_DIGEST_MD5", justification = "The fingerprint is just used to track new warnings")
FullTextFingerprint(final FileSystem fileSystem) {
this.fileSystem = fileSystem;
try {
digest = MessageDigest.getInstance("MD5"); // lgtm [java/weak-cryptographic-algorithm]
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
/**
* Creates a fingerprint of the specified issue using the source code at the affected line. The fingerprint is
* computed using the 1:1 content of a small number of lines before and after the affected line (see {@link
* #LINES_LOOK_AHEAD}).
*
* @param fileName
* the absolute path of the affected file
* @param line
* the line of the issue
* @param charset
* the encoding to be used when reading the affected file
*
* @return a fingerprint of the selected range of source code lines (if the file could not be read then the
* fingerprint actually is the hashcode of the filename)
* @throws IOException
* if the file could not be read
*/
public String compute(final String fileName, final int line, final Charset charset) throws IOException {
try (Stream lines = fileSystem.readLinesFromFile(fileName, charset)) {
return createFingerprint(line, lines, charset);
}
}
@VisibleForTesting
String getFallbackFingerprint(final String fileName) {
return String.format("%x", fileName.hashCode());
}
@VisibleForTesting
String createFingerprint(final int line, final Stream lines, final Charset charset) {
String context = extractContext(line, lines.iterator());
lines.close();
digest.update(context.getBytes(charset));
return asHex(digest.digest()).toUpperCase(Locale.ENGLISH);
}
private String asHex(final byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_CHARACTERS[v >>> 4];
hexChars[j * 2 + 1] = HEX_CHARACTERS[v & 0x0F];
}
return new String(hexChars);
}
@VisibleForTesting
String extractContext(final int affectedLine, final Iterator lines) {
if (affectedLine < 0) {
return StringUtils.EMPTY;
}
int start = computeStartLine(affectedLine);
StringBuilder context = new StringBuilder(LINE_RANGE_BUFFER_SIZE);
int line = 1;
for (; lines.hasNext() && line < start - LINES_LOOK_AHEAD; line++) {
lines.next(); // skip the first lines
}
for (; lines.hasNext() && line <= start + LINES_LOOK_AHEAD; line++) {
context.append(lines.next());
}
return context.toString();
}
private int computeStartLine(final int affectedLine) {
if (affectedLine == 0) { // indicates the whole file
return LINES_LOOK_AHEAD + 1;
}
else {
return affectedLine;
}
}
/**
* Facade for file system operations. May be replaced by stubs in test cases.
*/
@VisibleForTesting
static class FileSystem {
@MustBeClosed
Stream readLinesFromFile(final String fileName, final Charset charset)
throws IOException, InvalidPathException {
return Files.lines(Paths.get(fileName), charset);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy