package hudson.plugins.analysis.core;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import hudson.FilePath;
import hudson.plugins.analysis.Messages;
import hudson.plugins.analysis.util.FileFinder;
import hudson.plugins.analysis.util.model.FileAnnotation;
import hudson.plugins.analysis.util.model.Priority;
/**
* Stores the collection of parsed annotations and associated error messages. This class is not thread safe.
*
* @author Ulli Hafner
*/
public class ParserResult implements Serializable {
private static final long serialVersionUID = -8414545334379193330L;
private static final Logger LOGGER = Logger.getLogger(ParserResult.class.getName());
private static final String SLASH = "/";
/** The parsed annotations. */
@SuppressWarnings("Se")
private final Set<FileAnnotation> annotations = new HashSet<FileAnnotation>();
/** The collection of error messages. */
@SuppressWarnings("Se")
private final List<String> errorMessages = new ArrayList<String>();
/** Number of annotations by priority. */
@SuppressWarnings("Se")
private final Map<Priority, Integer> annotationCountByPriority = new HashMap<Priority, Integer>();
/** The set of modules. */
@SuppressWarnings("Se")
private final Set<String> modules = new HashSet<String>();
/** The workspace. */
private final Workspace workspace;
/** A mapping of relative file names to absolute file names. */
@SuppressWarnings("Se")
private final Multimap<String, String> fileNameCache = HashMultimap.create();
/** The log messages. @since 1.20 **/
private String logMessage;
/** Total number of modules. @since 1.31 **/
private int numberOfModules;
/**
* Determines whether relative paths in warnings should be resolved using a time expensive operation that scans the
* whole workspace for matching files.
*
* @since 1.55
*/
private final boolean canResolveRelativePaths;
/**
* Creates a new instance of {@link ParserResult}.
*/
public ParserResult() {
this(new NullWorkspace());
}
/**
* Creates a new instance of {@link ParserResult}.
*
* @param workspace the workspace to find the files in
*/
public ParserResult(final FilePath workspace) {
this(asWorkspace(workspace));
}
/**
* Creates a new instance of {@link ParserResult}.
*
* @param workspace the workspace to find the files in
*/
public ParserResult(final Workspace workspace) {
this(workspace, false);
}
/**
* Creates a new instance of {@link ParserResult}.
*
* @param workspace the workspace to find the files in
* @param canResolveRelativePaths determines whether relative paths in warnings should be resolved using a time
* expensive operation that scans the whole workspace for matching files
*/
public ParserResult(final FilePath workspace, boolean canResolveRelativePaths) {
this(asWorkspace(workspace), canResolveRelativePaths);
}
/**
* Creates a new instance of {@link ParserResult}.
*
* @param workspace the workspace to find the files in
* @param canResolveRelativePaths determines whether relative paths in warnings should be resolved using a time
* expensive operation that scans the whole workspace for matching files
*/
public ParserResult(final Workspace workspace, final boolean canResolveRelativePaths) {
this.workspace = workspace;
this.canResolveRelativePaths = canResolveRelativePaths;
for (Priority priority : Priority.values()) {
annotationCountByPriority.put(priority, 0);
}
}
private static FilePathAdapter asWorkspace(final FilePath workspace) {
return new FilePathAdapter(workspace);
}
/**
* Creates a new instance of {@link ParserResult}.
*
* @param annotations the annotations to add
*/
public ParserResult(final Collection<? extends FileAnnotation> annotations) {
this(new NullWorkspace());
addAnnotations(annotations);
}
/**
* Adds the warnings of the specified project to this project.
*
* @param additionalProject the project to add
*/
public void addProject(final ParserResult additionalProject) {
addAnnotations(additionalProject.getAnnotations());
addErrors(additionalProject.getErrorMessages());
addModules(additionalProject.getModules());
}
/**
* Finds a file with relative filename and replaces the name with the absolute path.
*
* @param annotation the annotation
*/
// TODO: when used on a slave then for each file a remote call is initiated
private void expandRelativePaths(final FileAnnotation annotation) {
try {
if (hasRelativeFileName(annotation)) {
Workspace remoteFile = workspace.child(annotation.getFileName());
if (remoteFile.exists()) {
annotation.setFileName(remoteFile.getPath());
}
else if (canResolveRelativePaths) {
findFileByScanningAllWorkspaceFiles(annotation);
}
}
}
catch (IOException exception) {
// ignore
}
catch (InterruptedException exception) {
// ignore
}
}
/**
* Returns the file name from the cache of all workspace files. The cache will be built only once.
*
* @param annotation the annotation to get the filename for
* @throws IOException signals that an I/O exception has occurred.
* @throws InterruptedException If the user cancels this action
*/
private void findFileByScanningAllWorkspaceFiles(final FileAnnotation annotation) throws IOException, InterruptedException {
if (fileNameCache.isEmpty()) {
populateFileNameCache();
}
String baseName = FilenameUtils.getName(annotation.getFileName());
if (fileNameCache.containsKey(baseName)) {
int matchesCount = 0;
String absoluteFileName = null;
for (String match : fileNameCache.get(baseName)) {
String annotationFileName = annotation.getFileName();
String strippedFileName = stripRelativePrefix(annotationFileName);
if (match.contains(strippedFileName)) {
absoluteFileName = workspace.getPath() + SLASH + match;
matchesCount++;
}
}
if (matchesCount == 1) {
annotation.setFileName(absoluteFileName);
}
else if (matchesCount == 0) {
LOGGER.log(Level.FINE, String.format(
"Absolute filename could not be resolved for: %s. Found no matches in cache: %s. ",
annotation.getFileName(), fileNameCache.get(baseName)));
}
else {
LOGGER.log(Level.FINE, String.format(
"Absolute filename could not be resolved for: %s. Found multiple matches in cache: %s. ",
annotation.getFileName(), fileNameCache.get(baseName)));
}
}
else {
LOGGER.log(Level.FINE, String.format(
"Absolute filename could not be resolved for: %s. No such file in workspace: %s. ",
annotation.getFileName(), workspace.getPath()));
}
}
String stripRelativePrefix(final String annotationFileName) {
return StringUtils.removePattern(annotationFileName, ".*(\\.\\.?/)+");
}
/**
* Builds a cache of file names in the remote file system.
*
* @throws IOException if the file could not be read
* @throws InterruptedException if the user cancels the search
*/
// TODO: Maybe the file pattern should be exposed on the UI in order to speed up the scanning, see JENKINS-2927
private void populateFileNameCache() throws IOException, InterruptedException {
LOGGER.log(Level.FINE, "Building cache of all workspace files to obtain absolute filenames for all warnings: " + workspace.getPath());
String[] allFiles = workspace.findFiles("**/*");
for (String file : allFiles) {
fileNameCache.put(FilenameUtils.getName(file), FilenameUtils.separatorsToUnix(file));
}
}
/**
* Returns whether the annotation references a relative filename.
*
* @param annotation the annotation
* @return <code>true</code> if the filename is relative
*/
private boolean hasRelativeFileName(final FileAnnotation annotation) {
String fileName = annotation.getFileName();
return StringUtils.isNotBlank(fileName) && !fileName.startsWith(SLASH) && !fileName.contains(":");
}
/**
* Adds the specified annotation to this container.
*
* @param annotation the annotation to add
* @return the number of added annotations
*/
@WithBridgeMethods(value = void.class) // JENKINS-25405
public final int addAnnotation(final FileAnnotation annotation) {
expandRelativePaths(annotation);
if (annotations.add(annotation)) {
Integer count = annotationCountByPriority.get(annotation.getPriority());
annotationCountByPriority.put(annotation.getPriority(), count + 1);
return 1;
}
return 0;
}
/**
* Adds the specified annotations to this container.
*
* @param newAnnotations the annotations to add
* @return the number of added annotations
*/
@WithBridgeMethods(value = void.class) // JENKINS-25405
public final int addAnnotations(final Collection<? extends FileAnnotation> newAnnotations) {
int count = 0;
for (FileAnnotation annotation : newAnnotations) {
count += addAnnotation(annotation);
}
return count;
}
/**
* Adds the specified annotations to this container.
*
* @param newAnnotations the annotations to add
*/
public final void addAnnotations(final FileAnnotation[] newAnnotations) {
addAnnotations(Arrays.asList(newAnnotations));
}
/**
* Adds an error message for the specified module name.
*
* @param module the current module
* @param message the error message
*/
public void addErrorMessage(final String module, final String message) {
errorMessages.add(Messages.Result_Error_ModuleErrorMessage(module, message));
}
/**
* Adds an error message.
*
* @param message the error message
*/
public void addErrorMessage(final String message) {
errorMessages.add(message);
}
/**
* Adds the error messages to this result.
*
* @param errors the error messages to add
*/
public void addErrors(final List<String> errors) {
errorMessages.addAll(errors);
}
/**
* Returns the errorMessages.
*
* @return the errorMessages
*/
public List<String> getErrorMessages() {
return ImmutableList.copyOf(errorMessages);
}
/**
* Returns the annotations of this result.
*
* @return the annotations of this result
*/
public Set<FileAnnotation> getAnnotations() {
return ImmutableSet.copyOf(annotations);
}
/**
* Returns the total number of annotations for this object.
*
* @return total number of annotations for this object
*/
public int getNumberOfAnnotations() {
return annotations.size();
}
/**
* Returns the total number of annotations of the specified priority for this object.
*
* @param priority the priority
* @return total number of annotations of the specified priority for this object
*/
public int getNumberOfAnnotations(final Priority priority) {
return annotationCountByPriority.get(priority);
}
/**
* Returns whether this objects has annotations.
*
* @return <code>true</code> if this objects has annotations.
*/
public boolean hasAnnotations() {
return !annotations.isEmpty();
}
/**
* Returns whether this objects has annotations with the specified priority.
*
* @param priority the priority
* @return <code>true</code> if this objects has annotations.
*/
public boolean hasAnnotations(final Priority priority) {
return annotationCountByPriority.get(priority) > 0;
}
/**
* Returns whether this objects has no annotations.
*
* @return <code>true</code> if this objects has no annotations.
*/
public boolean hasNoAnnotations() {
return !hasAnnotations();
}
/**
* Returns whether this objects has no annotations with the specified priority.
*
* @param priority the priority
* @return <code>true</code> if this objects has no annotations.
*/
public boolean hasNoAnnotations(final Priority priority) {
return !hasAnnotations(priority);
}
/**
* Returns the number of modules.
*
* @return the number of modules
*/
public int getNumberOfModules() {
return numberOfModules;
}
/**
* Returns the parsed modules.
*
* @return the parsed modules
*/
public Set<String> getModules() {
return Collections.unmodifiableSet(modules);
}
/**
* Adds a new parsed module.
*
* @param moduleName the name of the parsed module
*/
public void addModule(final String moduleName) {
modules.add(moduleName);
numberOfModules++;
}
/**
* Adds the specified parsed modules.
*
* @param additionalModules the name of the parsed modules
*/
public void addModules(final Collection<String> additionalModules) {
modules.addAll(additionalModules);
}
@Override
public String toString() {
return getNumberOfAnnotations() + " annotations";
}
/**
* Sets the log messages of the parsing process.
*
* @param message a multiline message
* @since 1.20
*/
public void setLog(final String message) {
logMessage = message;
}
/**
* Returns the log messages of the parsing process.
*
* @return the messages
* @since 1.20
*/
public String getLogMessages() {
return StringUtils.defaultString(logMessage);
}
/**
* Facade for the remote workspace.
*/
interface Workspace extends Serializable {
Workspace child(String fileName);
boolean exists() throws InterruptedException, IOException;
String getPath();
String[] findFiles(String pattern) throws IOException, InterruptedException;
}
/**
* Default implementation that delegates to a {@link FilePath} instance.
*/
private static class FilePathAdapter implements Workspace {
private static final long serialVersionUID = 1976601889843466249L;
private final FilePath wrapped;
/**
* Creates a new instance of {@link FilePathAdapter}.
*
* @param workspace the {@link FilePath} to wrap
*/
FilePathAdapter(final FilePath workspace) {
wrapped = workspace;
}
@Override
public Workspace child(final String fileName) {
return asWorkspace(wrapped.child(fileName));
}
@Override
public boolean exists() throws IOException, InterruptedException {
return wrapped.exists();
}
@Override
public String getPath() {
return wrapped.getRemote();
}
@Override
public String[] findFiles(final String pattern) throws IOException, InterruptedException {
return wrapped.act(new FileFinder(pattern));
}
}
/**
* Null pattern.
*/
private static class NullWorkspace implements Workspace {
private static final long serialVersionUID = 2307259492760554066L;
@Override
public Workspace child(final String fileName) {
return this;
}
@Override
public boolean exists() throws IOException, InterruptedException {
return false;
}
@Override
public String getPath() {
return StringUtils.EMPTY;
}
@Override
public String[] findFiles(final String pattern) throws IOException, InterruptedException {
return new String[0];
}
}
}