package hudson.plugins.global_build_stats.business;
import hudson.model.TopLevelItem;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
import hudson.plugins.global_build_stats.BuildResultStatusesConstants;
import hudson.plugins.global_build_stats.JobBuildResultFactory;
import hudson.plugins.global_build_stats.JobFilter;
import hudson.plugins.global_build_stats.JobFilterFactory;
import hudson.plugins.global_build_stats.GlobalBuildStatsPlugin;
import hudson.plugins.global_build_stats.model.BuildHistorySearchCriteria;
import hudson.plugins.global_build_stats.model.BuildResult;
import hudson.plugins.global_build_stats.model.BuildStatConfiguration;
import hudson.plugins.global_build_stats.model.DateRange;
import hudson.plugins.global_build_stats.model.JobBuildResult;
import hudson.util.DataSetBuilder;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;
public class GlobalBuildStatsBusiness {
private GlobalBuildStatsPlugin plugin;
public GlobalBuildStatsBusiness(GlobalBuildStatsPlugin _plugin){
this.plugin = _plugin;
}
public void onJobCompleted(AbstractBuild job){
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized (plugin) {
GlobalBuildStatsBusiness.addBuild(plugin.getJobBuildResults(), job);
try {
plugin.save();
} catch (IOException e) {
}
}
}
public BuildStatConfiguration searchBuildStatConfigById(String buildStatId){
int index = searchBuildStatConfigIndexById(buildStatId);
if(index != -1){
return plugin.getBuildStatConfigs().get(index);
} else {
return null;
}
}
private int searchBuildStatConfigIndexById(String id){
int idx = 0;
for(BuildStatConfiguration c : plugin.getBuildStatConfigs()){
if(id.equals(c.getId())){
break;
}
idx++;
}
if(idx == plugin.getBuildStatConfigs().size()){
idx = -1;
}
return idx;
}
public void recordBuildInfos() throws IOException {
List<JobBuildResult> jobBuildResultsRead = new ArrayList<JobBuildResult>();
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized (plugin) {
//TODO fix MatrixProject and use getAllJobs()
for (TopLevelItem item : Hudson.getInstance().getItems()) {
if (item instanceof AbstractProject) {
addBuildsFrom(jobBuildResultsRead, (AbstractProject) item);
}
}
plugin.setJobBuildResults(mergeJobBuildResults(plugin.getJobBuildResults(), jobBuildResultsRead));
plugin.save();
}
}
public JFreeChart createChart(BuildStatConfiguration config){
List<JobBuildResult> filteredJobBuildResults = createFilteredAndSortedBuildResults(config);
DataSetBuilder<String, DateRange> dsb = createDataSetBuilder(filteredJobBuildResults, config);
return createChart(config, dsb.build(), config.getBuildStatTitle());
}
public List<JobBuildResult> searchBuilds(BuildHistorySearchCriteria searchCriteria){
List<JobBuildResult> filteredJobBuildResults = new ArrayList<JobBuildResult>();
JobFilter jobFilter = JobFilterFactory.createJobFilter(searchCriteria.jobFilter);
for(JobBuildResult r : plugin.getJobBuildResults()){
if(r.getBuildDate().getTimeInMillis() >= searchCriteria.start
&& r.getBuildDate().getTimeInMillis() < searchCriteria.end
&& jobResultStatusMatchesWith(r.getResult(), searchCriteria)
&& jobFilter.isJobApplicable(r.getJobName())){
filteredJobBuildResults.add(r);
}
}
// Sorting on job results dates
sortJobBuildResultsByBuildDate(filteredJobBuildResults);
return filteredJobBuildResults;
}
public void updateBuildStatConfiguration(String buildStatId, BuildStatConfiguration config) throws IOException {
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized(plugin){
int buildStatIndex = searchBuildStatConfigIndexById(buildStatId);
plugin.getBuildStatConfigs().set(buildStatIndex, config);
plugin.save();
}
}
public void addBuildStatConfiguration(BuildStatConfiguration config) throws IOException {
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized(plugin){
plugin.getBuildStatConfigs().add(config);
plugin.save();
}
}
public void deleteBuildStatConfiguration(String buildStatId) throws IOException {
synchronized(plugin){
int index = searchBuildStatConfigIndexById(buildStatId);
plugin.getBuildStatConfigs().remove(index);
plugin.save();
}
}
public void moveUpConf(String buildStatId) throws IOException {
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized(plugin){
int index = searchBuildStatConfigIndexById(buildStatId);
BuildStatConfiguration b = plugin.getBuildStatConfigs().get(index);
// Swapping build confs
plugin.getBuildStatConfigs().set(index, plugin.getBuildStatConfigs().get(index-1));
plugin.getBuildStatConfigs().set(index-1, b);
plugin.save();
}
}
public void moveDownConf(String buildStatId) throws IOException {
// Synchronizing plugin instance every time we modify persisted informations on it
synchronized(plugin){
int index = searchBuildStatConfigIndexById(buildStatId);
BuildStatConfiguration b = plugin.getBuildStatConfigs().get(index);
// Swapping build confs
plugin.getBuildStatConfigs().set(index, plugin.getBuildStatConfigs().get(index+1));
plugin.getBuildStatConfigs().set(index+1, b);
plugin.save();
}
}
public static String escapeAntiSlashes(String value){
if(value != null){
return value.replaceAll("\\\\", "\\\\\\\\");
} else {
return null;
}
}
protected static boolean jobResultStatusMatchesWith(BuildResult r, BuildHistorySearchCriteria c){
return (BuildResult.ABORTED.equals(r) && c.abortedShown)
|| (BuildResult.FAILURE.equals(r) && c.failuresShown)
|| (BuildResult.NOT_BUILD.equals(r) && c.notBuildShown)
|| (BuildResult.SUCCESS.equals(r) && c.successShown)
|| (BuildResult.UNSTABLE.equals(r) && c.unstablesShown);
}
private JFreeChart createChart(final BuildStatConfiguration config, CategoryDataset dataset, String title) {
final JFreeChart chart = ChartFactory.createStackedAreaChart(title, null, "Count", dataset, PlotOrientation.VERTICAL, true, true, false);
chart.setBackgroundPaint(Color.white);
final LegendTitle legend = chart.getLegend();
legend.setPosition(RectangleEdge.RIGHT);
final CategoryPlot plot = chart.getCategoryPlot();
plot.setForegroundAlpha(0.85F);
plot.setRangeGridlinesVisible(true);
CategoryAxis domainAxis = new ShiftedCategoryAxis(null);
domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45);
domainAxis.setLowerMargin(0.0);
domainAxis.setUpperMargin(0.0);
domainAxis.setCategoryMargin(0.0);
plot.setDomainAxis(domainAxis);
final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
// This renderer allows to map area for clicks
// + it fixes some rendering bug (0 is displayed on "demi" tick instead of "plain" tick)
final StackedAreaRenderer2 renderer = new StackedAreaRenderer2(){
@Override
public String generateURL(CategoryDataset dataset, int row, int column) {
DateRange range = (DateRange) dataset.getColumnKey(column);
String status = (String) dataset.getRowKey(row);
boolean successShown=BuildResultStatusesConstants.SUCCESS.equals(status);
boolean failuresShown=BuildResultStatusesConstants.FAILURES.equals(status);
boolean unstablesShown=BuildResultStatusesConstants.UNSTABLES.equals(status);
boolean abortedShown=BuildResultStatusesConstants.ABORTED.equals(status);
boolean notBuildShown=BuildResultStatusesConstants.NOT_BUILD.equals(status);
return new StringBuilder()
.append("buildHistory?jobFilter=").append(config.getJobFilter())
.append("&start=").append(range.getStart().getTimeInMillis())
.append("&end=").append(range.getEnd().getTimeInMillis())
.append("&successShown=").append(successShown)
.append("&failuresShown=").append(failuresShown)
.append("&unstablesShown=").append(unstablesShown)
.append("&abortedShown=").append(abortedShown)
.append("¬BuildShown=").append(notBuildShown).toString();
}
/* TODO: add tooltip
@Override
public String generateToolTip(CategoryDataset dataset, int row, int column) {
NumberOnlyBuildLabel label = (NumberOnlyBuildLabel) dataset.getColumnKey(column);
AbstractTestResultAction a = label.build.getAction(AbstractTestResultAction.class);
switch (row) {
case 0:
return String.valueOf(Messages.AbstractTestResultAction_fail(a.getFailCount()));
case 1:
return String.valueOf(Messages.AbstractTestResultAction_skip(a.getSkipCount()));
default:
return String.valueOf(Messages.AbstractTestResultAction_test(a.getTotalCount()));
}
}*/
};
plot.setRenderer(renderer);
renderer.setSeriesPaint(0, new Color(255, 255, 85));
renderer.setSeriesPaint(1, new Color(255, 85, 85));
renderer.setSeriesPaint(2, new Color(85, 85, 85));
renderer.setSeriesPaint(3, new Color(85, 85, 255));
renderer.setSeriesPaint(4, new Color(255, 85, 255));
plot.setRenderer(renderer);
plot.setInsets(new RectangleInsets(5.0, 0, 0, 5.0));
return chart;
}
private DataSetBuilder<String, DateRange> createDataSetBuilder(List<JobBuildResult> filteredJobBuildResults,
BuildStatConfiguration config){
DataSetBuilder<String, DateRange> dsb = new DataSetBuilder<String, DateRange>();
if(filteredJobBuildResults.size() == 0){
return dsb;
}
Calendar d2 = new GregorianCalendar();
Calendar d1 = config.getHistoricScale().getPreviousStep(d2);
int nbSuccess=0, nbFailures=0, nbUnstables=0, nbAborted=0, nbNotBuild=0;
int nbSteps = 0;
Iterator<JobBuildResult> buildsIter = filteredJobBuildResults.iterator();
JobBuildResult currentBuild = buildsIter.next();
Calendar buildDate = currentBuild.getBuildDate();
while(nbSteps != config.getHistoricLength()){
// Finding range where the build resides
while(nbSteps < config.getHistoricLength() && d1.after(buildDate)){
DateRange range = new DateRange(d1, d2, config.getHistoricScale().getDateRangeFormatter());
dsb.add(nbSuccess, BuildResultStatusesConstants.SUCCESS, range);
dsb.add(nbFailures, BuildResultStatusesConstants.FAILURES, range);
dsb.add(nbUnstables, BuildResultStatusesConstants.UNSTABLES, range);
dsb.add(nbAborted, BuildResultStatusesConstants.ABORTED, range);
dsb.add(nbNotBuild, BuildResultStatusesConstants.NOT_BUILD, range);
d2 = (Calendar)d1.clone();
d1 = config.getHistoricScale().getPreviousStep(d2);
nbSuccess=0; nbFailures=0; nbUnstables=0; nbAborted=0; nbNotBuild=0;
nbSteps++;
}
// If no range found : stop the iteration !
if(nbSteps != config.getHistoricLength() && currentBuild != null){
nbSuccess += config.isSuccessShown()?currentBuild.getResult().getSuccessCount():0;
nbFailures += config.isFailuresShown()?currentBuild.getResult().getFailureCount():0;
nbUnstables += config.isUnstablesShown()?currentBuild.getResult().getUnstableCount():0;
nbAborted += config.isAbortedShown()?currentBuild.getResult().getAbortedCount():0;
nbNotBuild += config.isNotBuildShown()?currentBuild.getResult().getNotBuildCount():0;
if(buildsIter.hasNext()){
currentBuild = buildsIter.next();
buildDate = currentBuild.getBuildDate();
} else {
currentBuild = null;
buildDate = new GregorianCalendar(); buildDate.setTimeInMillis(1);
}
}
}
return dsb;
}
private static void sortJobBuildResultsByBuildDate(List<JobBuildResult> c){
Collections.sort(c, Collections.reverseOrder(new Comparator<JobBuildResult>() {
public int compare(JobBuildResult o1, JobBuildResult o2) {
return o1.getBuildDate().compareTo(o2.getBuildDate());
}
}));
}
private List<JobBuildResult> createFilteredAndSortedBuildResults(BuildStatConfiguration config){
List<JobBuildResult> filteredJobBuildResults = new ArrayList<JobBuildResult>();
for(JobBuildResult r : plugin.getJobBuildResults()){
if(JobFilterFactory.createJobFilter(config.getJobFilter()).isJobApplicable(r.getJobName())){
filteredJobBuildResults.add(r);
}
}
// Sorting on job results dates
sortJobBuildResultsByBuildDate(filteredJobBuildResults);
return filteredJobBuildResults;
}
private static void addBuild(List<JobBuildResult> jobBuildResultsRead, AbstractBuild build){
jobBuildResultsRead.add(JobBuildResultFactory.INSTANCE.createJobBuildResult(build));
}
private static void addBuildsFrom(List<JobBuildResult> jobBuildResultsRead, AbstractProject project){
List<AbstractBuild> builds = project.getBuilds();
Iterator<AbstractBuild> buildIterator = builds.iterator();
while (buildIterator.hasNext()) {
addBuild(jobBuildResultsRead, buildIterator.next());
}
}
protected static List<JobBuildResult> mergeJobBuildResults(List<JobBuildResult> existingJobResults, List<JobBuildResult> jobResultsToMerge){
List<JobBuildResult> mergedJobResultsList = new ArrayList<JobBuildResult>(existingJobResults);
for(JobBuildResult jbrToMerge : jobResultsToMerge){
if(!mergedJobResultsList.contains(jbrToMerge)){
mergedJobResultsList.add(jbrToMerge);
}
}
return mergedJobResultsList;
}
}