package nl.topicus.onderwijs.dashboard.modules.hudson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import nl.topicus.onderwijs.dashboard.config.ISettings;
import nl.topicus.onderwijs.dashboard.datasources.HudsonAlerts;
import nl.topicus.onderwijs.dashboard.datasources.HudsonBuildNumber;
import nl.topicus.onderwijs.dashboard.datasources.HudsonBuildStatus;
import nl.topicus.onderwijs.dashboard.datasources.NumberOfUnitTests;
import nl.topicus.onderwijs.dashboard.datatypes.Alert;
import nl.topicus.onderwijs.dashboard.datatypes.DotColor;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.Build;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.BuildReference;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.Hudson;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.Job;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.JobReference;
import nl.topicus.onderwijs.dashboard.datatypes.hudson.Result;
import nl.topicus.onderwijs.dashboard.keys.Key;
import nl.topicus.onderwijs.dashboard.keys.Project;
import nl.topicus.onderwijs.dashboard.modules.AbstractService;
import nl.topicus.onderwijs.dashboard.modules.DashboardRepository;
import nl.topicus.onderwijs.dashboard.modules.ServiceConfiguration;
import nl.topicus.onderwijs.dashboard.modules.topicus.RetrieverUtils;
import nl.topicus.onderwijs.dashboard.modules.topicus.StatusPageResponse;
import org.codehaus.jackson.map.DeserializationConfig.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@ServiceConfiguration(interval = 1, unit = TimeUnit.MINUTES)
public class HudsonService extends AbstractService {
private static final Logger log = LoggerFactory
.getLogger(HudsonService.class);
private final ObjectMapper mapper = new ObjectMapper();
private Map<HudsonKey<BuildReference>, Build> buildsCache = new HashMap<HudsonKey<BuildReference>, Build>();
private ConcurrentHashMap<Project, List<Job>> jobsCache = new ConcurrentHashMap<Project, List<Job>>();
private ConcurrentHashMap<String, Alert> alertsCache = new ConcurrentHashMap<String, Alert>();
@Autowired
public HudsonService(ISettings settings) {
super(settings);
mapper.getDeserializationConfig().disable(
Feature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setDateFormat(new HudsonDateFormat());
}
@Override
public void onConfigure(DashboardRepository repository) {
Map<Key, Map<String, ?>> serviceSettings = getSettings()
.getServiceSettings(HudsonService.class);
for (Key key : serviceSettings.keySet()) {
if (key instanceof Project) {
Project project = (Project) key;
repository.addDataSource(project, NumberOfUnitTests.class,
new NumberOfUnitTestsImpl(project, this));
repository.addDataSource(project, HudsonBuildStatus.class,
new HudsonBuildStatusImpl(project, this));
repository.addDataSource(project, HudsonBuildNumber.class,
new HudsonBuildNumberImpl(project, this));
repository.addDataSource(project, HudsonAlerts.class,
new HudsonAlertsImpl(project, this));
}
}
}
public void refreshData() {
try {
Map<Key, Map<String, ?>> serviceSettings = getSettings()
.getServiceSettings(HudsonService.class);
for (Entry<Key, Map<String, ?>> entry : serviceSettings.entrySet()) {
if (!(entry.getKey() instanceof Project))
continue;
Project project = (Project) entry.getKey();
Map<String, ?> hudsonSettingsForProject = entry.getValue();
String url = hudsonSettingsForProject.get("url").toString();
@SuppressWarnings("unchecked")
Map<String, String> patterns = (Map<String, String>) hudsonSettingsForProject
.get("matchers");
if (!url.endsWith("/"))
url = url + "/";
StatusPageResponse response = RetrieverUtils.getStatuspage(url
+ "api/json");
if (response.getHttpStatusCode() != 200) {
return;
}
Hudson hudson = mapper.readValue(response.getPageContent(),
Hudson.class);
for (JobReference jobReference : hudson.getJobs()) {
String name = jobReference.getName();
for (Entry<String, String> patternEntry : patterns
.entrySet()) {
if (Pattern.matches(patternEntry.getValue(), name)) {
refreshData(project, jobReference,
patternEntry.getKey());
}
}
}
}
} catch (Exception e) {
log.error("Unable to refresh data from hudson: {} {}", e.getClass()
.getSimpleName(), e.getMessage());
}
}
private void refreshData(Project project, JobReference jobReference,
String code) throws Exception {
StatusPageResponse response = RetrieverUtils.getStatuspage(jobReference
.getUrl() + "api/json");
if (response.getHttpStatusCode() != 200) {
return;
}
Job job = mapper.readValue(response.getPageContent(), Job.class);
job.setCode(code);
List<Job> jobs = jobsCache.putIfAbsent(project, new ArrayList<Job>());
if (jobs == null)
jobs = jobsCache.get(project);
for (Job job2 : jobs) {
if (job2.getName().equals(job.getName())) {
jobs.remove(job2);
break;
}
}
jobs.add(job);
jobsCache.put(project, jobs);
}
public List<Job> getJobs(Project project) {
List<Job> list = jobsCache.get(project);
return list == null ? new ArrayList<Job>() : new ArrayList<Job>(list);
}
public List<Build> getBuilds(Project project) {
List<Job> jobs = getJobs(project);
List<Build> builds = new ArrayList<Build>();
for (Job job : jobs) {
for (int i = 0; i < Math.min(5, job.getBuilds().size()); i++) {
BuildReference buildReference = job.getBuilds().get(i);
Build build = getBuild(project, buildReference, job);
builds.add(build);
}
}
Collections.sort(builds, new BuildsComparator());
return builds;
}
public Build getBuild(Project project, BuildReference reference, Job job) {
if (buildsCache.containsKey(HudsonKey.of(project, reference))) {
return buildsCache.get(HudsonKey.of(project, reference));
}
if (buildsCache.size() > 1000) {
buildsCache.clear();
}
try {
StatusPageResponse response = RetrieverUtils
.getStatuspage(reference.getUrl() + "api/json");
if (response.getHttpStatusCode() != 200) {
return null;
}
Build build = mapper.readValue(response.getPageContent(),
Build.class);
build.setJob(job);
if (!build.isBuilding()) {
// don't store the build result in the cache when it's still
// building.
buildsCache.put(HudsonKey.of(project, reference), build);
}
return build;
} catch (Exception e) {
log.error(
"Unable to retrieve project " + project.getName()
+ " build " + reference.getNumber() + " from "
+ reference.getUrl(), e);
return null;
}
}
public List<Alert> getAlerts(Project project) {
List<Job> jobs = getJobs(project);
Map<String, Build> builds = new HashMap<String, Build>();
for (Job job : jobs) {
for (int i = 0; i < Math.min(1, job.getBuilds().size()); i++) {
BuildReference buildReference = job.getBuilds().get(i);
Build build = getBuild(project, buildReference, job);
builds.put(job.getName(), build);
}
}
List<Alert> ret = new ArrayList<Alert>();
for (Job curJob : jobs) {
Build curBuild = builds.get(curJob.getName());
if (curBuild == null)
continue;
if (Result.UNSTABLE.equals(curBuild.getResult())) {
Alert alert = new Alert(alertsCache.get(curJob.getName()),
DotColor.YELLOW, project, "Build "
+ curBuild.getNumber() + " is unstable");
alert.setOverlayVisible((System.currentTimeMillis() - curBuild
.getTimestamp().getTime()) < 90 * 1000);
alertsCache.put(curJob.getName(), alert);
ret.add(alert);
} else if (Result.FAILURE.equals(curBuild.getResult())) {
Alert alert = new Alert(alertsCache.get(curJob.getName()),
DotColor.RED, project, "Build " + curBuild.getNumber()
+ " failed");
alert.setOverlayVisible((System.currentTimeMillis() - curBuild
.getTimestamp().getTime()) < 90 * 1000);
alertsCache.put(curJob.getName(), alert);
ret.add(alert);
} else
alertsCache.remove(curJob.getName());
}
return ret;
}
public static class BuildsComparator implements Comparator<Build> {
@Override
public int compare(Build o1, Build o2) {
return o2.getTimestamp().compareTo(o1.getTimestamp());
}
}
public static class HudsonKey<T> {
private final Project project;
private final T reference;
public HudsonKey(Project project, T reference) {
this.project = project;
this.reference = reference;
}
public Project getProject() {
return project;
}
public T getReference() {
return reference;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((project == null) ? 0 : project.hashCode());
result = prime * result
+ ((reference == null) ? 0 : reference.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HudsonKey<?> other = (HudsonKey<?>) obj;
if (project == null) {
if (other.project != null)
return false;
} else if (!project.equals(other.project))
return false;
if (reference == null) {
if (other.reference != null)
return false;
} else if (!reference.equals(other.reference))
return false;
return true;
}
static <R> HudsonKey<R> of(Project p, R reference) {
return new HudsonKey<R>(p, reference);
}
}
}