package org.netmelody.cieye.spies.jenkins;
import static com.google.common.collect.Collections2.filter;
import static com.google.common.collect.Collections2.transform;
import static com.google.common.collect.Lists.newArrayList;
import static java.util.Collections.unmodifiableSet;
import static org.netmelody.cieye.core.domain.Percentage.percentageOf;
import static org.netmelody.cieye.core.domain.RunningBuild.buildAt;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.netmelody.cieye.core.domain.RunningBuild;
import org.netmelody.cieye.core.domain.Sponsor;
import org.netmelody.cieye.core.domain.Status;
import org.netmelody.cieye.core.domain.TargetDetail;
import org.netmelody.cieye.core.observation.KnownOffendersDirectory;
import org.netmelody.cieye.spies.jenkins.jsondomain.Build;
import org.netmelody.cieye.spies.jenkins.jsondomain.BuildDetail;
import org.netmelody.cieye.spies.jenkins.jsondomain.ChangeSetItem;
import org.netmelody.cieye.spies.jenkins.jsondomain.Job;
import org.netmelody.cieye.spies.jenkins.jsondomain.JobDetail;
import org.netmelody.cieye.spies.jenkins.jsondomain.User;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
public final class JobAnalyser {
private final JenkinsCommunicator communicator;
private final String jobEndpoint;
private final Map<String, Set<Sponsor>> sponsorCache = new HashMap<String, Set<Sponsor>>();
private final KnownOffendersDirectory detective;
private final BuildDetailFetcher buildDetailFetcher;
private final BuildDurationFetcher buildDurationFetcher;
private final BuildStartTimeFetcher buildStartTimeFetcher;
public JobAnalyser(JenkinsCommunicator communicator, String jobEndpoint, KnownOffendersDirectory detective) {
this.communicator = communicator;
this.jobEndpoint = jobEndpoint;
this.detective = detective;
this.buildDetailFetcher = new BuildDetailFetcher(this.communicator);
this.buildDurationFetcher = new BuildDurationFetcher(this.buildDetailFetcher);
this.buildStartTimeFetcher = new BuildStartTimeFetcher(this.buildDetailFetcher);
}
public TargetDetail analyse(Job jobDigest) {
if (!jobDigest.url.equals(jobEndpoint)) {
throw new IllegalArgumentException("Incorrect job digest");
}
final JobDetail job = communicator.jobDetailFor(jobEndpoint);
if (!job.building() && Status.BROKEN != job.status()) {
sponsorCache.clear();
return new TargetDetail(job.url, job.url, job.name, job.status(), startTimeOf(job));
}
return new TargetDetail(job.url, job.url, job.name, statusOf(job), startTimeOf(job), buildsFor(job), sponsorsOf(job));
}
public String lastBadBuildUrl() {
return communicator.lastBadBuildFor(jobEndpoint);
}
private long startTimeOf(JobDetail job) {
return buildStartTimeFetcher.lastStartTimeOf(job);
}
private Status statusOf(JobDetail job) {
if (job.lastBuild == null) {
return job.status();
}
if (!Status.BROKEN.equals(job.status())) {
return job.status();
}
final String lastBadBuildDesc = this.buildDetailFetcher.detailsOf(job.lastBadBuildUrl()).description;
if (null == lastBadBuildDesc || lastBadBuildDesc.length() == 0) {
return job.status();
}
return Status.UNDER_INVESTIGATION;
}
private Set<Sponsor> sponsorsOf(JobDetail job) {
final Set<Sponsor> result = new HashSet<Sponsor>();
if (job.lastBuild == null) {
return result;
}
long lastSuccessNumber = (job.lastStableBuild == null) ? -1 : job.lastStableBuild.number;
for (Build build : job.builds()) {
if (build.number > lastSuccessNumber) {
result.addAll(sponsorsOf(build.url));
}
}
return result;
}
private Set<Sponsor> sponsorsOf(String buildUrl) {
if (null == buildUrl || buildUrl.length() == 0) {
return new HashSet<Sponsor>();
}
if (sponsorCache.containsKey(buildUrl)) {
return sponsorCache.get(buildUrl);
}
final BuildDetail buildData = this.buildDetailFetcher.detailsOf(buildUrl);
if (null == buildData) {
return new HashSet<Sponsor>();
}
final Set<Sponsor> sponsors = new HashSet<Sponsor>(detective.search(commitMessagesOf(buildData)));
if (sponsors.isEmpty()) {
for (String upstreamBuildUrl : buildData.upstreamBuildUrls()) {
sponsors.addAll(sponsorsOf(communicator.endpoint() + "/" + upstreamBuildUrl));
}
}
final Set<Sponsor> result = unmodifiableSet(sponsors);
if (!buildData.building) {
sponsorCache.put(buildUrl, result);
}
return result;
}
private String commitMessagesOf(BuildDetail build) {
if (null == build.changeSet || null == build.changeSet.items) {
return "";
}
final StringBuilder result = new StringBuilder();
for (ChangeSetItem changeSetItem : build.changeSet.items) {
result.append(changeSetItem.user);
result.append(' ');
result.append(changeSetItem.msg);
result.append(' ');
}
for (User user : build.culprits()) {
result.append(user.fullName);
result.append(' ');
}
return result.toString();
}
private List<RunningBuild> buildsFor(final JobDetail job) {
if (!job.building() || job.lastBuild == null) {
return newArrayList();
}
final long lastCompletedJobNumber = (job.lastCompletedBuild == null) ? 0L : job.lastCompletedBuild.number;
final Collection<Build> builds = newArrayList(filter(job.builds(), after(lastCompletedJobNumber)));
if (builds.isEmpty()) {
builds.add(job.lastBuild);
}
final long duration = this.buildDurationFetcher.lastGoodDurationOf(job);
return newArrayList(transform(filter(transform(builds, toBuildDetail()), building()), toRunningBuild(duration)));
}
private Predicate<Build> after(final long lastCompletedJobNumber) {
return new Predicate<Build>() {
@Override public boolean apply(Build build) { return build.number > lastCompletedJobNumber; }
};
}
private Function<Build, BuildDetail> toBuildDetail() {
return new Function<Build, BuildDetail>() {
@Override public BuildDetail apply(Build build) {
return buildDetailFetcher.detailsOf(build.url);
}
};
}
private Predicate<BuildDetail> building() {
return new Predicate<BuildDetail>() {
@Override public boolean apply(BuildDetail build) { return build.building; }
};
}
private Function<BuildDetail, RunningBuild> toRunningBuild(final long duration) {
return new Function<BuildDetail, RunningBuild>() {
@Override public RunningBuild apply(BuildDetail buildDetail) {
return buildAt(percentageOf(new Date().getTime() - buildDetail.timestamp, duration));
}
};
}
}