package hudson.plugins.analysis.views;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.NoSuchElementException;
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 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.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 AbstractBuild<?, ?> 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 AbstractBuild<?, ?> 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()));
}
splitSourceFile(highlightSource(file));
}
catch (IOException exception) {
sourceCode = "Can't read file: " + exception.getLocalizedMessage();
}
finally {
IOUtils.closeQuietly(file);
}
}
@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 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
*/
public AbstractBuild<?, ?> getOwner() {
return owner;
}
/**
* Returns the line that should be highlighted.
*
* @return the line to highlight
*/
public String getSourceCode() {
return sourceCode;
}
}