package hudson.plugins.analysis.core; import javax.annotation.CheckForNull; import java.io.File; import java.io.IOException; 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.lang.StringUtils; import org.kohsuke.stapler.StaplerProxy; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.FilePath; import hudson.maven.AggregatableAction; import hudson.maven.MavenAggregatedReport; import hudson.maven.MavenBuild; import hudson.maven.MavenModule; import hudson.model.AbstractBuild; import hudson.model.Run; import hudson.model.HealthReport; import hudson.model.Result; import hudson.plugins.analysis.util.PluginLogger; import hudson.plugins.analysis.util.StringPluginLogger; import hudson.plugins.analysis.util.ToolTipProvider; import hudson.plugins.analysis.util.model.AbstractAnnotation; /** * Base class for Maven aggregated build reports. * * @author Ulli Hafner * @since 1.20 * @param <T> type of the build result */ public abstract class MavenResultAction<T extends BuildResult> implements StaplerProxy, AggregatableAction, MavenAggregatedReport, ResultAction<T> { /** The default encoding to be used when reading and parsing files. */ private final String defaultEncoding; /** Reuse all the functionality of the action for freestyle jobs. */ private final AbstractResultAction<T> delegate; private transient StringPluginLogger logger; private transient Set<MavenModule> modules = Sets.newHashSet(); private final transient String pluginName; /** * Creates a new instance of {@link MavenResultAction}. * * @param delegate * result action for freestyle jobs that will do the main of the * work * @param defaultEncoding * the default encoding to be used when reading and parsing files * @param pluginName * name of the plug-in */ public MavenResultAction(final AbstractResultAction<T> delegate, final String defaultEncoding, final String pluginName) { this.defaultEncoding = defaultEncoding; this.delegate = delegate; this.pluginName = pluginName; } @Override public abstract Class<? extends MavenResultAction<T>> getIndividualActionType(); /** * Returns the URL of the 24x24 image used in the build link. * * @return the URL of the image * @since 1.42 */ public String getSmallImageName() { return delegate.getSmallImageName(); } /** * Creates a new build result that contains the aggregated results. * * @param existingResult * the already existing result * @param additionalResult * the additional result to be aggregated with the existing * result * @return the created result */ protected abstract T createResult(T existingResult, T additionalResult); /** * Called whenever a new module build is completed, to update the aggregated * report. When multiple builds complete simultaneously, Jenkins serializes * the execution of this method, so this method needs not be * concurrency-safe. * * @param moduleBuilds * Same as <tt>MavenModuleSet.getModuleBuilds()</tt> but provided * for convenience and efficiency. * @param newBuild * Newly completed build. */ @Override public void update(final Map<MavenModule, List<MavenBuild>> moduleBuilds, final MavenBuild newBuild) { MavenResultAction<T> additionalAction = newBuild.getAction(getIndividualActionType()); MavenModule project = newBuild.getProject(); if (additionalAction != null && !getModules().contains(project)) { T existingResult = delegate.getResult(); T additionalResult = additionalAction.getResult(); if (newBuild.getResult().isBetterThan(Result.FAILURE) || additionalResult.getPluginResult().isWorseOrEqualTo(Result.FAILURE)) { getModules().add(project); setResult(createAggregatedResult(existingResult, additionalResult)); copySourceFilesToModuleBuildFolder(newBuild); } } } private void copySourceFilesToModuleBuildFolder(final MavenBuild newBuild) { FilePath filePath = new FilePath(new File(newBuild.getRootDir(), AbstractAnnotation.WORKSPACE_FILES)); try { filePath.copyRecursiveTo("*.tmp", new FilePath(new File(getOwner().getRootDir(), AbstractAnnotation.WORKSPACE_FILES))); } catch (IOException exception) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, "Can't copy workspace files: ", exception); } catch (InterruptedException exception) { // ignore, user canceled the operation } } private T createAggregatedResult(@CheckForNull final T existingResult, final T additionalResult) { T createdResult = createResult(existingResult, additionalResult); if (new NullHealthDescriptor(delegate.getHealthDescriptor()).isThresholdEnabled()) { createdResult.evaluateStatus(additionalResult.getThresholds(), additionalResult.canUseDeltaValues(), getLogger(), getUrlName()); } return createdResult; } /** * Aggregates the results in a new instance of {@link ParserResult}. * * @param existingResult * an already existing result, might be <code>null</code> for the * first aggregation * @param additionalResult * the additional result to be aggregated with the existing * result * @return the aggregated result */ protected ParserResult aggregate(@CheckForNull final T existingResult, final T additionalResult) { ParserResult aggregatedAnnotations = new ParserResult(); List<BuildResult> results = Lists.newArrayList(); if (existingResult != null) { results.add(existingResult); } results.add(additionalResult); for (BuildResult result : results) { aggregatedAnnotations.addAnnotations(result.getAnnotations()); aggregatedAnnotations.addModules(result.getModules()); aggregatedAnnotations.addErrors(result.getErrors()); } return aggregatedAnnotations; } private PluginLogger getLogger() { if (logger == null) { logger = createLogger(); } return logger; } private StringPluginLogger createLogger() { return new StringPluginLogger("[" + StringUtils.defaultString(pluginName, "ANALYSIS") + "] "); // NOCHECKSTYLE } private Set<MavenModule> getModules() { if (modules == null) { modules = Sets.newHashSet(); } return modules; } /** * Returns all logging statements of this action that couldn't be printed so far. * * @return the logging statements */ public String getLog() { String message = getLogger().toString(); logger = createLogger(); return message; } /** * Returns the default encoding. * * @return the default encoding */ public String getDefaultEncoding() { return defaultEncoding; } /** * Returns whether a large image is defined. * * @return <code>true</code> if a large image is defined, <code>false</code> * otherwise. If no large image is defined, then the attribute * {@code icon} must to be provided in jelly tag {@code summary}. * @since 1.41 */ public boolean hasLargeImage() { return delegate.hasLargeImage(); } /** * Returns the URL of the 48x48 image used in the build summary. * * @return the URL of the image * @since 1.41 */ public String getLargeImageName() { return delegate.getLargeImageName(); } @Override public String getIconFileName() { return delegate.getIconFileName(); } @Override public String getDisplayName() { return delegate.getDisplayName(); } @Override public String getUrlName() { return delegate.getUrlName(); } // CHECKSTYLE:OFF @Override public void setResult(final T additionalResult) { delegate.setResult(additionalResult); } @Override public T getResult() { return delegate.getResult(); } /** * Determines whether only stable builds should be used as reference builds * or not. * * @since 1.73 * @return <code>true</code> if only stable builds should be used */ public boolean useOnlyStableBuildsAsReference() { return delegate.getResult().useOnlyStableBuildsAsReference(); } /** * Determines whether to always use the previous build as the reference. * * @since 1.73 * @return <code>true</code> if the previous build should always be used. */ public boolean usePreviousBuildAsStable() { return delegate.getResult().usePreviousBuildAsStable(); } /** * Returns the associated build of this action. * * @return the associated build of this action */ @WithBridgeMethods(value=AbstractBuild.class, adapterMethod="getAbstractBuild") public Run<?, ?> getOwner() { return delegate.getOwner(); } /** * 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 Object getAbstractBuild(final Run owner, final Class targetClass) { return delegate.getOwner() instanceof AbstractBuild ? (AbstractBuild<?, ?>) delegate.getOwner() : null; } /** * Returns the health of this action. * * @return the health of this action */ public final HealthReport getBuildHealth() { return delegate.getBuildHealth(); } @Override public ToolTipProvider getToolTipProvider() { return delegate.getToolTipProvider(); } @Override @WithBridgeMethods(value=AbstractBuild.class, adapterMethod="getAbstractBuild") public final Run<?, ?> getBuild() { return delegate.getOwner(); } @Override public final Object getTarget() { return delegate.getTarget(); } /** * Returns the tooltip for the specified number of items. * * @param numberOfItems * the number of items to display the tooltip for * @return the tooltip for the specified items */ public String getTooltip(final int numberOfItems) { return delegate.getTooltip(numberOfItems); } @Override public boolean isSuccessful() { return delegate.isSuccessful(); } @Override public AbstractHealthDescriptor getHealthDescriptor() { return delegate.getHealthDescriptor(); } // CHECKSTYLE:ON }