package com.capitalone.dashboard.collector;
import com.capitalone.dashboard.model.Build;
import com.capitalone.dashboard.model.BuildStatus;
import com.capitalone.dashboard.model.HudsonJob;
import com.capitalone.dashboard.model.SCM;
import com.capitalone.dashboard.util.Supplier;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* HudsonClient implementation that uses RestTemplate and JSONSimple to
* fetch information from Hudson instances.
*/
@Component
public class DefaultHudsonClient implements HudsonClient {
private static final Log LOG = LogFactory.getLog(DefaultHudsonClient.class);
private final RestOperations rest;
private final HudsonSettings settings;
private static final String JOBS_URL_SUFFIX = "/api/json?tree=jobs[name,url,builds[number,url]]";
private static final String[] CHANGE_SET_ITEMS_TREE = new String[] {
"user",
"author[fullName]",
"revision",
"id",
"msg",
"timestamp",
"date",
"paths[file]"
};
private static final String[] BUILD_DETAILS_TREE = new String[] {
"number",
"url",
"timestamp",
"duration",
"building",
"result",
"culprits[fullName]",
"changeSet[items[" + StringUtils.join(CHANGE_SET_ITEMS_TREE, ",") + "]",
"revisions[module,revision]]"
};
private static final String BUILD_DETAILS_URL_SUFFIX = "/api/json?tree=" + StringUtils.join(BUILD_DETAILS_TREE, ",");
@Autowired
public DefaultHudsonClient(Supplier<RestOperations> restOperationsSupplier, HudsonSettings settings) {
this.rest = restOperationsSupplier.get();
this.settings = settings;
}
@Override
public Map<HudsonJob, Set<Build>> getInstanceJobs(String instanceUrl) {
Map<HudsonJob, Set<Build>> result = new LinkedHashMap<>();
try {
String url = StringUtils.removeEnd(instanceUrl, "/") + JOBS_URL_SUFFIX;
ResponseEntity<String> responseEntity = makeRestCall(URI.create(url));
String returnJSON = responseEntity.getBody();
JSONParser parser = new JSONParser();
try {
JSONObject object = (JSONObject) parser.parse(returnJSON);
for (Object job : getJsonArray(object, "jobs")) {
JSONObject jsonJob = (JSONObject) job;
HudsonJob hudsonJob = new HudsonJob();
hudsonJob.setInstanceUrl(instanceUrl);
hudsonJob.setJobName(getString(jsonJob, "name"));
hudsonJob.setJobUrl(getString(jsonJob, "url"));
Set<Build> builds = new LinkedHashSet<>();
result.put(hudsonJob, builds);
for (Object build : getJsonArray(jsonJob, "builds")) {
JSONObject jsonBuild = (JSONObject) build;
// A basic Build object. This will be fleshed out later if this is a new Build.
String buildNumber = jsonBuild.get("number").toString();
if (!buildNumber.equals("0")) {
Build hudsonBuild = new Build();
hudsonBuild.setNumber(buildNumber);
hudsonBuild.setBuildUrl(getString(jsonBuild, "url"));
builds.add(hudsonBuild);
}
}
}
} catch (ParseException e) {
LOG.error("Parsing jobs on instance: " + instanceUrl, e);
}
} catch (RestClientException rce) {
LOG.error(rce);
}
return result;
}
@Override
public Build getBuildDetails(String buildUrl) {
try {
String url = StringUtils.removeEnd(buildUrl, "/") + BUILD_DETAILS_URL_SUFFIX;
ResponseEntity<String> result = makeRestCall(URI.create(url));
String returnJSON = result.getBody();
JSONParser parser = new JSONParser();
try {
JSONObject buildJson = (JSONObject) parser.parse(returnJSON);
Boolean building = (Boolean) buildJson.get("building");
// Ignore jobs that are building
if (!building) {
Build build = new Build();
build.setNumber(buildJson.get("number").toString());
build.setBuildUrl(buildUrl);
build.setTimestamp(System.currentTimeMillis());
build.setStartTime((Long) buildJson.get("timestamp"));
build.setDuration((Long) buildJson.get("duration"));
build.setEndTime(build.getStartTime() + build.getDuration());
build.setBuildStatus(getBuildStatus(buildJson));
build.setStartedBy(firstCulprit(buildJson));
if (settings.isSaveLog()) {
build.setLog(getLog(buildUrl));
}
addChangeSets(build, buildJson);
return build;
}
} catch (ParseException e) {
LOG.error("Parsing build: " + buildUrl, e);
}
} catch (RestClientException rce) {
LOG.error(rce);
}
return null;
}
/**
* Grabs changeset information for the given build.
*
* @param build a Build
* @param buildJson the build JSON object
*/
private void addChangeSets(Build build, JSONObject buildJson) {
JSONObject changeSet = (JSONObject) buildJson.get("changeSet");
Map<String, String> revisionToUrl = new HashMap<>();
// Build a map of revision to module (scm url). This is not always
// provided by the Hudson API, but we can use it if available.
for (Object revision : getJsonArray(changeSet, "revisions")) {
JSONObject json = (JSONObject) revision;
revisionToUrl.put(json.get("revision").toString(), getString(json, "module"));
}
for (Object item : getJsonArray(changeSet, "items")) {
JSONObject jsonItem = (JSONObject) item;
SCM scm = new SCM();
scm.setScmAuthor(getCommitAuthor(jsonItem));
scm.setScmCommitLog(getString(jsonItem, "msg"));
scm.setScmCommitTimestamp(getCommitTimestamp(jsonItem));
scm.setScmRevisionNumber(getRevision(jsonItem));
scm.setScmUrl(revisionToUrl.get(scm.getScmRevisionNumber()));
scm.setNumberOfChanges(getJsonArray(jsonItem, "paths").size());
build.getSourceChangeSet().add(scm);
}
}
////// Helpers
private long getCommitTimestamp(JSONObject jsonItem) {
if (jsonItem.get("timestamp") != null) {
return (Long) jsonItem.get("timestamp");
} else if (jsonItem.get("date") != null) {
String dateString = (String) jsonItem.get("date");
try {
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").parse(dateString).getTime();
} catch (java.text.ParseException e) {
// Try an alternate date format...looks like this one is used by Git
try {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(dateString).getTime();
} catch (java.text.ParseException e1) {
LOG.error("Invalid date string: " + dateString, e);
}
}
}
return 0;
}
private String getString(JSONObject json, String key) {
return (String) json.get(key);
}
private String getRevision(JSONObject jsonItem) {
// Use revision if provided, otherwise use id
Long revision = (Long) jsonItem.get("revision");
return revision == null ? getString(jsonItem, "id") : revision.toString();
}
private JSONArray getJsonArray(JSONObject json, String key) {
Object array = json.get(key);
return array == null ? new JSONArray() : (JSONArray) array;
}
private String firstCulprit(JSONObject buildJson) {
JSONArray culprits = getJsonArray(buildJson, "culprits");
if (culprits.isEmpty()) {
return null;
}
JSONObject culprit = (JSONObject) culprits.get(0);
return getFullName(culprit);
}
private String getFullName(JSONObject author) {
return getString(author, "fullName");
}
private String getCommitAuthor(JSONObject jsonItem) {
// Use user if provided, otherwise use author.fullName
JSONObject author = (JSONObject) jsonItem.get("author");
return author == null ? getString(jsonItem, "user") : getFullName(author);
}
private BuildStatus getBuildStatus(JSONObject buildJson) {
String status = buildJson.get("result").toString();
switch(status) {
case "SUCCESS": return BuildStatus.Success;
case "UNSTABLE": return BuildStatus.Unstable;
case "FAILURE": return BuildStatus.Failure;
case "ABORTED": return BuildStatus.Aborted;
default: return BuildStatus.Unknown;
}
}
private ResponseEntity<String> makeRestCall(URI uri) {
// Basic Auth only.
if (StringUtils.isNotEmpty(this.settings.getUsername())
&& StringUtils.isNotEmpty(this.settings.getApiKey())) {
return rest.exchange(uri, HttpMethod.GET,
new HttpEntity<>(createHeaders(this.settings.getUsername(), this.settings.getApiKey())),
String.class);
} else {
return rest.exchange(uri, HttpMethod.GET, null,
String.class);
}
}
private HttpHeaders createHeaders(final String userId, final String password) {
return new HttpHeaders() {
{
String auth = userId + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.US_ASCII));
String authHeader = "Basic " + new String(encodedAuth);
set(HttpHeaders.AUTHORIZATION, authHeader);
}
};
}
private String getLog(String buildUrl) {
ResponseEntity<String> responseEntity = makeRestCall(
URI.create(buildUrl + "consoleText"));
String returnJSON = responseEntity.getBody();
return returnJSON;
}
}