package hudson.plugins.analysis.core; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collection; import org.apache.commons.lang.StringUtils; import org.apache.maven.project.MavenProject; import hudson.FilePath; import hudson.maven.MavenBuildProxy; import hudson.maven.MavenBuildProxy.BuildCallable; import hudson.maven.MavenReporter; import hudson.maven.MojoInfo; import hudson.maven.MavenBuild; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.Result; import hudson.plugins.analysis.Messages; import hudson.plugins.analysis.util.PluginLogger; import hudson.plugins.analysis.util.model.AbstractAnnotation; import hudson.plugins.analysis.util.model.AnnotationContainer; import hudson.plugins.analysis.util.model.DefaultAnnotationContainer; import hudson.plugins.analysis.util.model.FileAnnotation; import hudson.plugins.analysis.util.model.Priority; import hudson.plugins.analysis.util.model.WorkspaceFile; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; import hudson.tasks.BuildStep; /** * A base class for maven reporters with the following two characteristics: * <ul> * <li>It provides a unstable threshold, that could be enabled and set in the * configuration screen. If the number of annotations in a build exceeds this * value then the build is considered as {@link Result#UNSTABLE UNSTABLE}. * </li> * <li>It provides thresholds for the build health, that could be adjusted in * the configuration screen. These values are used by the * {@link HealthReportBuilder} to compute the health and the health trend graph.</li> * </ul> * * @author Ulli Hafner */ // CHECKSTYLE:COUPLING-OFF @SuppressWarnings("PMD.TooManyFields") public abstract class HealthAwareMavenReporter extends MavenReporter implements HealthDescriptor { /** Default threshold priority limit. */ private static final String DEFAULT_PRIORITY_THRESHOLD_LIMIT = "low"; /** Unique identifier of this class. */ private static final long serialVersionUID = 3003791883748835331L; /** Annotation threshold to be reached if a build should be considered as unstable. */ private final String threshold; /** Annotation threshold to be reached if a build should be considered as failure. */ private final String failureThreshold; /** Threshold for new annotations to be reached if a build should be considered as failure. */ private final String newFailureThreshold; /** Annotation threshold for new warnings to be reached if a build should be considered as unstable. */ private final String newThreshold; /** Report health as 100% when the number of warnings is less than this value. */ private final String healthy; /** Report health as 0% when the number of warnings is greater than this value. */ private final String unHealthy; /** The name of the plug-in. */ private final String pluginName; /** Determines which warning priorities should be considered when evaluating the build stability and health. */ private String thresholdLimit; /** The default encoding to be used when reading and parsing files. */ private String defaultEncoding; /** Determines whether the plug-in should run for failed builds, too. @since 1.6 */ private final boolean canRunOnFailed; /** * Creates a new instance of <code>HealthReportingMavenReporter</code>. * * @param threshold * Annotations threshold to be reached if a build should be * considered as unstable. * @param newThreshold * New annotations threshold to be reached if a build should be * considered as unstable. * @param failureThreshold * Annotation threshold to be reached if a build should be considered as * failure. * @param newFailureThreshold * New annotations threshold to be reached if a build should be * considered as failure. * @param healthy * Report health as 100% when the number of warnings is less than * this value * @param unHealthy * Report health as 0% when the number of warnings is greater * than this value * @param thresholdLimit * determines which warning priorities should be considered when * evaluating the build stability and health * @param canRunOnFailed * determines whether the plug-in can run for failed builds, too * @param pluginName * the name of the plug-in */ // CHECKSTYLE:OFF public HealthAwareMavenReporter(final String threshold, final String newThreshold, final String failureThreshold, final String newFailureThreshold, final String healthy, final String unHealthy, final String thresholdLimit, final boolean canRunOnFailed, final String pluginName) { super(); this.threshold = threshold; this.newThreshold = newThreshold; this.failureThreshold = failureThreshold; this.newFailureThreshold = newFailureThreshold; this.healthy = healthy; this.unHealthy = unHealthy; this.thresholdLimit = thresholdLimit; this.canRunOnFailed = canRunOnFailed; this.pluginName = "[" + pluginName + "] "; System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.Jdk14Logger"); } // CHECKSTYLE:ON /** * Creates a new instance of <code>HealthReportingMavenReporter</code>. * * @param threshold * Annotations threshold to be reached if a build should be * considered as unstable. * @param newThreshold * New annotations threshold to be reached if a build should be * considered as unstable. * @param failureThreshold * Annotation threshold to be reached if a build should be considered as * failure. * @param newFailureThreshold * New annotations threshold to be reached if a build should be * considered as failure. * @param healthy * Report health as 100% when the number of warnings is less than * this value * @param unHealthy * Report health as 0% when the number of warnings is greater * than this value * @param thresholdLimit * determines which warning priorities should be considered when * evaluating the build stability and health * @param pluginName * the name of the plug-in * @deprecated replaced by {@link #HealthAwareMavenReporter(String, String, String, String, String, String, String, boolean, String)} */ // CHECKSTYLE:OFF @Deprecated public HealthAwareMavenReporter(final String threshold, final String newThreshold, final String failureThreshold, final String newFailureThreshold, final String healthy, final String unHealthy, final String thresholdLimit, final String pluginName) { this(threshold, newThreshold, failureThreshold, newFailureThreshold, healthy, unHealthy, thresholdLimit, false, pluginName); } // CHECKSTYLE:ON /** * Initializes new fields that are not serialized yet. * * @return the object */ @edu.umd.cs.findbugs.annotations.SuppressWarnings("Se") private Object readResolve() { if (thresholdLimit == null) { thresholdLimit = DEFAULT_PRIORITY_THRESHOLD_LIMIT; } return this; } /** {@inheritDoc} */ @SuppressWarnings({"serial", "PMD.AvoidFinalLocalVariable"}) @Override public final boolean postExecute(final MavenBuildProxy build, final MavenProject pom, final MojoInfo mojo, final BuildListener listener, final Throwable error) throws InterruptedException, IOException { PluginLogger logger = new PluginLogger(listener.getLogger(), pluginName); if (!acceptGoal(mojo.getGoal())) { return true; } Result currentResult = getCurrentResult(build); if (!canContinue(currentResult)) { logger.log("Skipping reporter since build result is " + currentResult); return true; } if (hasResultAction(build)) { logger.log("Skipping maven reporter: there is already a result available."); return true; } final ParserResult result = perform(build, pom, mojo, logger); defaultEncoding = pom.getProperties().getProperty("project.build.sourceEncoding"); if (defaultEncoding == null) { logger.log(Messages.Reporter_Error_NoEncoding(Charset.defaultCharset().displayName())); result.addErrorMessage(pom.getName(), Messages.Reporter_Error_NoEncoding(Charset.defaultCharset().displayName())); } build.execute(new BuildCallable<Void, IOException>() { public Void call(final MavenBuild mavenBuild) throws IOException, InterruptedException { persistResult(result, mavenBuild); return null; } }); copyFilesWithAnnotationsToBuildFolder(logger, build.getRootDir(), result.getAnnotations()); return true; } /** * Returns the current result of the build. * * @param build * the build proxy * @return the current result of the build * @throws IOException * if the results could not be read * @throws InterruptedException * if the user canceled the operation */ private Result getCurrentResult(final MavenBuildProxy build) throws IOException, InterruptedException { return build.execute(new BuildResultCallable()); } /** * Returns whether this plug-in can run for failed builds, too. * * @return the can run on failed */ public boolean getCanRunOnFailed() { return canRunOnFailed; } /** * Returns whether this reporter can continue processing. This default * implementation returns <code>true</code> if the property * <code>canRunOnFailed</code> is set or if the build is not aborted or * failed. * * @param result * build result * @return <code>true</code> if the build can continue */ protected boolean canContinue(final Result result) { if (canRunOnFailed) { return result != Result.ABORTED; } else { return result != Result.ABORTED && result != Result.FAILURE; } } /** * Copies all files with annotations from the workspace to the build folder. * * @param logger * logger to log any problems * @param buildRoot * directory to store the copied files in * @param annotations * annotations determining the actual files to copy * @throws IOException * if the files could not be written * @throws FileNotFoundException * if the files could not be written * @throws InterruptedException * if the user cancels the processing */ private void copyFilesWithAnnotationsToBuildFolder(final PluginLogger logger, final FilePath buildRoot, final Collection<FileAnnotation> annotations) throws IOException, FileNotFoundException, InterruptedException { FilePath directory = new FilePath(buildRoot, AbstractAnnotation.WORKSPACE_FILES); if (!directory.exists()) { directory.mkdirs(); } AnnotationContainer container = new DefaultAnnotationContainer(annotations); for (WorkspaceFile file : container.getFiles()) { FilePath masterFile = new FilePath(directory, file.getTempName()); if (!masterFile.exists()) { try { new FilePath((Channel)null, file.getName()).copyTo(masterFile); } catch (IOException exception) { String message = "Can't copy source file: source=" + file.getName() + ", destination=" + masterFile.getName(); logger.log(message); logger.printStackTrace(exception); } } } } /** * Determines whether this plug-in will accept the specified goal. The * {@link #postExecute(MavenBuildProxy, MavenProject, MojoInfo, * BuildListener, Throwable)} will only by invoked if the plug-in returns * <code>true</code>. * * @param goal the maven goal * @return <code>true</code> if the plug-in accepts this goal */ protected abstract boolean acceptGoal(final String goal); /** * Performs the publishing of the results of this plug-in. * * @param build * the build proxy (on the slave) * @param pom * the pom of the module * @param mojo * the executed mojo * @param logger * the logger to report the progress to * @return the java project containing the found annotations * @throws InterruptedException * If the build is interrupted by the user (in an attempt to * abort the build.) Normally the {@link BuildStep} * implementations may simply forward the exception it got from * its lower-level functions. * @throws IOException * If the implementation wants to abort the processing when an * {@link IOException} happens, it can simply propagate the * exception to the caller. This will cause the build to fail, * with the default error message. Implementations are * encouraged to catch {@link IOException} on its own to provide * a better error message, if it can do so, so that users have * better understanding on why it failed. */ protected abstract ParserResult perform(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, PluginLogger logger) throws InterruptedException, IOException; /** * Persists the result in the build (on the master). * * @param project * the created project * @param build * the build (on the master) * @return the created result */ protected abstract BuildResult persistResult(ParserResult project, MavenBuild build); /** * Returns the default encoding derived from the maven pom file. * * @return the default encoding */ protected String getDefaultEncoding() { return defaultEncoding; } /** * Returns whether we already have a result for this build. * * @param build * the current build. * @return <code>true</code> if we already have a task result action. * @throws IOException * in case of an IO error * @throws InterruptedException * if the call has been interrupted */ @SuppressWarnings("serial") private Boolean hasResultAction(final MavenBuildProxy build) throws IOException, InterruptedException { return build.execute(new BuildCallable<Boolean, IOException>() { public Boolean call(final MavenBuild mavenBuild) throws IOException, InterruptedException { return mavenBuild.getAction(getResultActionClass()) != null; } }); } /** * Returns the type of the result action. * * @return the type of the result action */ protected abstract Class<? extends Action> getResultActionClass(); /** * Returns the path to the target folder. * * @param pom the maven pom * @return the path to the target folder */ protected FilePath getTargetPath(final MavenProject pom) { return new FilePath((VirtualChannel)null, pom.getBuild().getDirectory()); } /** * Returns the threshold of all annotations to be reached if a build should * be considered as unstable. * * @return the threshold of all annotations to be reached if a build should * be considered as unstable. */ public String getThreshold() { return threshold; } /** * Returns the threshold for new annotations to be reached if a build should * be considered as unstable. * * @return the threshold for new annotations to be reached if a build should * be considered as unstable. */ public String getNewThreshold() { return newThreshold; } /** * Returns the annotation threshold to be reached if a build should be * considered as failure. * * @return the annotation threshold to be reached if a build should be * considered as failure. */ public String getFailureThreshold() { return failureThreshold; } /** * Returns the threshold of new annotations to be reached if a build should * be considered as failure. * * @return the threshold of new annotations to be reached if a build should * be considered as failure. */ public String getNewFailureThreshold() { return newFailureThreshold; } /** * Returns the healthy threshold, i.e. when health is reported as 100%. * * @return the 100% healthiness */ public String getHealthy() { return healthy; } /** * Returns the unhealthy threshold, i.e. when health is reported as 0%. * * @return the 0% unhealthiness */ public String getUnHealthy() { return unHealthy; } /** {@inheritDoc} */ public Priority getMinimumPriority() { return Priority.valueOf(StringUtils.upperCase(getThresholdLimit())); } /** * Returns the threshold limit. * * @return the threshold limit */ public String getThresholdLimit() { return thresholdLimit; } /** * Returns the name of the module. * * @param pom * the pom * @return the name of the module */ protected String getModuleName(final MavenProject pom) { return StringUtils.defaultString(pom.getName(), pom.getArtifactId()); } /** * Gets the build result from the master. */ private static final class BuildResultCallable implements BuildCallable<Result, IOException> { /** Unique ID. */ private static final long serialVersionUID = -270795641776014760L; /** {@inheritDoc} */ public Result call(final MavenBuild mavenBuild) throws IOException, InterruptedException { return mavenBuild.getResult(); } } /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient boolean thresholdEnabled; /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient int minimumAnnotations; /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient int healthyAnnotations; /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient int unHealthyAnnotations; /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient boolean healthyReportEnabled; /** Backward compatibility. @deprecated */ @SuppressWarnings("unused") @Deprecated private transient String height; }