Skip to content

Instantly share code, notes, and snippets.

@h3xstream
Created August 11, 2018 04:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save h3xstream/912f96d3cc65cdb1aac07596cc4d9d61 to your computer and use it in GitHub Desktop.
Save h3xstream/912f96d3cc65cdb1aac07596cc4d9d61 to your computer and use it in GitHub Desktop.
SourceDetail with prism.js
package hudson.plugins.analysis.views;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import de.java2html.converter.JavaSource2HTMLConverter;
import de.java2html.javasource.JavaSource;
import de.java2html.javasource.JavaSourceParser;
import de.java2html.options.JavaSourceConversionOptions;
import hudson.model.AbstractBuild;
import hudson.model.Run;
import hudson.model.ModelObject;
import hudson.plugins.analysis.util.EncodingValidator;
import hudson.plugins.analysis.util.model.FileAnnotation;
import hudson.plugins.analysis.util.model.LineRange;
/**
* Renders a source file containing an annotation for the whole file or a
* specific line number.
*
* @author Ulli Hafner
*/
@SuppressWarnings("PMD.CyclomaticComplexity")
public class SourceDetail implements ModelObject {
/** Offset of the source code generator. After this line the actual source file lines start. */
protected static final int SOURCE_GENERATOR_OFFSET = 13;
/** Color for the first (primary) annotation range. */
private static final String FIRST_COLOR = "#FCAF3E";
/** Color for all other annotation ranges. */
private static final String OTHER_COLOR = "#FCE94F";
/** The current build as owner of this object. */
private final Run<?, ?> owner;
/** Stripped file name of this annotation without the path prefix. */
private final String fileName;
/** The annotation to be shown. */
private final FileAnnotation annotation;
/** The rendered source file. */
private String sourceCode = StringUtils.EMPTY;
/** The default encoding to be used when reading and parsing files. */
private final String defaultEncoding;
/**
* Creates a new instance of this source code object.
*
* @param owner
* the current build as owner of this object
* @param annotation
* the warning to display in the source file
* @param defaultEncoding
* the default encoding to be used when reading and parsing files
*/
public SourceDetail(final Run<?, ?> owner, final FileAnnotation annotation, final String defaultEncoding) {
this.owner = owner;
this.annotation = annotation;
this.defaultEncoding = defaultEncoding;
fileName = StringUtils.substringAfterLast(annotation.getFileName(), "/");
initializeContent();
}
/**
* Initializes the content of the source file: reads the file, colors it, and
* splits it into three parts.
*/
private void initializeContent() {
InputStream file = null;
try {
File tempFile = new File(annotation.getTempName(owner));
if (tempFile.exists()) {
file = new FileInputStream(tempFile);
}
else {
file = new FileInputStream(new File(annotation.getFileName()));
}
sourceCode = buildCodeBlock(file,annotation.getFileName());
//splitSourceFile(highlightSource(file));
}
catch (IOException exception) {
sourceCode = "Can't read file: " + exception.getLocalizedMessage();
}
finally {
IOUtils.closeQuietly(file);
}
}
private String prismLangClassFromExtension(String extension) {
switch(extension.toLowerCase()) {
case "jav": case "java":
return "language-java";
case "htm": case "html":
return "language-markup";
case "erb": case "jsp": case "tag":
return "language-erb";
case "rb":
return "language-ruby";
case "kt":
return "language-kotlin";
case "js":
return "language-javascript";
case "c":
return "language-c";
case "cs":
return "language-csharp";
case "vb":
return "language-vbnet";
case "cpp":
return "language-cpp";
case "groovy":
return "language-groovy";
case "pl":
return "language-perl";
case "php":
return "language-php";
case "py":
return "language-python";
case "scala": case "sc":
return "language-scala";
}
return "language-clike"; //Best effort for unknown extensions
}
private String buildCodeBlock(InputStream inputStream, String fileName) throws IOException {
StringWriter writer = new StringWriter();
String prismCssClass = prismLangClassFromExtension(FilenameUtils.getExtension(fileName));
writer.append("<pre><code class=\""+prismCssClass+" line-numbers\">");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
List<LineRange> ranges = new ArrayList<>(annotation.getLineRanges());
LineRange activeRange = null;
boolean activeRegularBlock = false;
int lineNumber = 0;
String line = null;
while((line = reader.readLine()) != null) {
lineNumber++;
/** Start highlight block **/
if(activeRange == null) {
boolean newBlock = false;
ranges: for(LineRange r :ranges) {
if(r.getStart() == lineNumber) {
newBlock = true;
activeRange = r;
break ranges;
}
}
if(newBlock) {
writer.append("</code><code class=\"highlight\">");
}
}
/** Code **/
writer.append(line);
writer.append("\n");
/** Clode highlight block with bug description **/
if(activeRange != null) {
if (lineNumber == activeRange.getEnd()) { //Closing active block
ranges.remove(activeRange);
activeRange = null;
writer.append("</code>");
writer.append("<div class=\"analysis-warning\"><i class=\"fas fa-exclamation-triangle\"></i> " +
"<span class=\"analysis-warning-title\">"+annotation.getMessage() + "</span>\n");
writer.append("<div class=\"analysis-detail\">");
writer.append(annotation.getToolTip());
writer.append("</div></div>\n");
writer.append("<code>");
}
}
}
IOUtils.copy(inputStream, writer, "UTF-8");
writer.append("</code></pre>");
return writer.toString();
}
@Override
public String getDisplayName() {
return fileName;
}
/**
* Highlights the specified source and returns the result as an HTML string.
*
* @param file
* the source file to highlight
* @return the source as an HTML string
* @throws IOException
* if the source code could not be read
*/
public final String highlightSource(final InputStream file) throws IOException {
JavaSource source = new JavaSourceParser().parse(
new InputStreamReader(file, EncodingValidator.defaultCharset(defaultEncoding)));
JavaSource2HTMLConverter converter = new JavaSource2HTMLConverter();
StringWriter writer = new StringWriter();
JavaSourceConversionOptions options = JavaSourceConversionOptions.getDefault();
options.setShowLineNumbers(true);
options.setAddLineAnchors(true);
converter.convert(source, options, writer);
return writer.toString();
}
/**
* Splits the source code into three blocks: the line to highlight and the
* source code before and after this line.
*
* @param sourceFile
* the source code of the whole file as rendered HTML string
*/
// CHECKSTYLE:CONSTANTS-OFF
public final void splitSourceFile(final String sourceFile) {
StringBuilder output = new StringBuilder(sourceFile.length());
LineIterator lineIterator = IOUtils.lineIterator(new StringReader(sourceFile));
int lineNumber = 1;
try {
while (lineNumber < SOURCE_GENERATOR_OFFSET) {
copyLine(output, lineIterator);
lineNumber++;
}
lineNumber = 1;
boolean isFirstRange = true;
for (LineRange range : annotation.getLineRanges()) {
while (lineNumber < range.getStart()) {
copyLine(output, lineIterator);
lineNumber++;
}
output.append("</code>\n");
output.append("</td></tr>\n");
output.append("<tr><td style=\"background-color:");
appendRangeColor(output, isFirstRange);
output.append("\">\n");
output.append("<div id=\"line");
output.append(range.getStart());
output.append("\" tooltip=\"");
if (range.getStart() > 0) {
outputEscaped(output, annotation.getMessage());
}
outputEscaped(output, annotation.getToolTip());
output.append("\" nodismiss=\"\">\n");
output.append("<code><b>\n");
if (range.getStart() <= 0) {
output.append(annotation.getMessage());
if (StringUtils.isBlank(annotation.getMessage())) {
output.append(annotation.getToolTip());
}
}
else {
while (lineNumber <= range.getEnd()) {
copyLine(output, lineIterator);
lineNumber++;
}
}
output.append("</b></code>\n");
output.append("</div>\n");
output.append("</td></tr>\n");
output.append("<tr><td>\n");
output.append("<code>\n");
isFirstRange = false;
}
while (lineIterator.hasNext()) {
copyLine(output, lineIterator);
}
}
catch (NoSuchElementException exception) {
// ignore an illegal range
}
sourceCode = output.toString();
}
// CHECKSTYLE:CONSTANTS-ON
/**
* Writes the message to the output stream (with escaped HTML).
* @param output the output to write to
* @param message
* the message to write
*/
private void outputEscaped(final StringBuilder output, final String message) {
output.append(StringEscapeUtils.escapeHtml(message));
}
/**
* Appends the right range color.
*
* @param output the output to append the color
* @param isFirstRange determines whether the range is the first one
*/
private void appendRangeColor(final StringBuilder output, final boolean isFirstRange) {
if (isFirstRange) {
output.append(FIRST_COLOR);
}
else {
output.append(OTHER_COLOR);
}
}
/**
* Copies the next line of the input to the output.
*
* @param output output
* @param lineIterator input
*/
private void copyLine(final StringBuilder output, final LineIterator lineIterator) {
output.append(lineIterator.nextLine());
output.append("\n");
}
/**
* Gets the file name of this source file.
*
* @return the file name
*/
public String getFileName() {
return fileName;
}
/**
* Returns the build as owner of this object.
*
* @return the build
*/
@WithBridgeMethods(value=AbstractBuild.class, adapterMethod="getAbstractBuild")
public Run<?, ?> getOwner() {
return owner;
}
/**
* Added for backward compatibility. It generates <pre>AbstractBuild getOwner()</pre> bytecode during the build
* process, so old implementations can use that signature.
*
* @see {@link WithBridgeMethods}
*/
@Deprecated
private final Object getAbstractBuild(Run owner, Class targetClass) {
return owner instanceof AbstractBuild ? (AbstractBuild) owner : null;
}
/**
* Returns the line that should be highlighted.
*
* @return the line to highlight
*/
public String getSourceCode() {
return sourceCode;
}
/**
* Creates a new instance of this source code object.
*
* @param owner
* the current build as owner of this object
* @param annotation
* the warning to display in the source file
* @param defaultEncoding
* the default encoding to be used when reading and parsing files
* @deprecated use {@link #SourceDetail(Run, FileAnnotation, String)} instead
*/
@Deprecated
public SourceDetail(final AbstractBuild<?, ?> owner, final FileAnnotation annotation, final String defaultEncoding) {
this((Run<?, ?>) owner, annotation, defaultEncoding);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment