package com.sgoertzen.sonarbreak; import com.fasterxml.jackson.databind.ObjectMapper; import com.sgoertzen.sonarbreak.qualitygate.CeResponse; import com.sgoertzen.sonarbreak.qualitygate.Query; import com.sgoertzen.sonarbreak.qualitygate.Result; import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.logging.Log; import org.joda.time.DateTime; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.text.DateFormat; import java.text.SimpleDateFormat; /** * Execute a query against Sonar to fetch the quality gate status for a build. This will look for a sonar status that * matches the current build number and that we run in the last minute. The query will wait up to ten minutes for * the results to become available on the sonar server. */ public class QueryExecutor { public static final String SONAR_FORMAT_PATH = "api/measures/component?componentKey=%s" + "&metricKeys=quality_gate_details"; public static final String SONAR_ANALYSIS_TIME_PATH = "api/ce/component?componentKey=%s"; public static final int SONAR_CONNECTION_RETRIES = 10; public static final int SONAR_PROCESSING_WAIT_TIME = 10000; // wait time between sonar checks in milliseconds private final URL sonarURL; private final int sonarLookBackSeconds; private final int waitForProcessingSeconds; private final Log log; /** * Creates a new executor for running queries against sonar. * * @param sonarServer Fully qualified URL to the sonar server * @param sonarLookBackSeconds Amount of time to look back into sonar history for the results of this build * @param waitForProcessingSeconds Amount of time to wait for sonar to finish processing the job * @param log Log for logging @return Results indicating if the build passed the sonar quality gate checks * @throws MalformedURLException If the sonar url is invalid */ public QueryExecutor(String sonarServer, int sonarLookBackSeconds, int waitForProcessingSeconds, Log log) throws MalformedURLException { this.sonarURL = new URL(sonarServer); this.sonarLookBackSeconds = sonarLookBackSeconds; this.waitForProcessingSeconds = waitForProcessingSeconds; this.log = log; } /** * Execute the given query on the specified sonar server. * * @param query The query specifying the project and version of the build * @return Result fetched from sonar * @throws SonarBreakException If there is an issues communicating with sonar * @throws IOException If the url is invalid */ public Result execute(Query query) throws SonarBreakException, IOException { URL analysisQueryUrl = buildUrl(sonarURL,query, SONAR_ANALYSIS_TIME_PATH); log.debug(String.format("Built a sonar query url of: %s" , analysisQueryUrl.toString())); URL queryUrl = buildUrl(sonarURL, query, SONAR_FORMAT_PATH); log.debug(String.format("Built a sonar query url of: %s", queryUrl.toString())); if (!isURLAvailable(sonarURL, SONAR_CONNECTION_RETRIES)) { throw new SonarBreakException(String.format("Unable to get a valid response after %d tries", SONAR_CONNECTION_RETRIES)); } return fetchSonarStatusWithRetries(analysisQueryUrl, queryUrl); } /** * Creates a url for the specified quality gate query * * @param sonarUrl The sonar server we will be querying * @param query Holds details on the query we want to make * @return A URL object representing the query * @throws MalformedURLException If the sonar url is not valid * @throws IllegalArgumentException If the sonar key is not valid */ protected static URL buildUrl(URL sonarUrl, Query query, String path) throws MalformedURLException, IllegalArgumentException { if (query.getSonarKey() == null || query.getSonarKey().length() == 0) { throw new IllegalArgumentException("No resource specified in the Query"); } String sonarPathWithResource = String.format(path, query.getSonarKey()); return new URL(sonarUrl, sonarPathWithResource); } /** * Get the status from sonar for the currently executing build. This waits for sonar to complete its processing * before returning the results. * * @param queryUrl The sonar URL to get the results from * @return Matching result object for this build * @throws IOException * @throws SonarBreakException */ private Result fetchSonarStatusWithRetries(URL analysisQueryUrl, URL queryUrl) throws IOException, SonarBreakException { DateTime oneMinuteAgo = DateTime.now().minusSeconds(sonarLookBackSeconds); DateTime waitUntil = DateTime.now().plusSeconds(waitForProcessingSeconds); do { // If this is the first time the job is running on sonar the URL might not be available. Return null and wait. if (isAnalysisAvailable(analysisQueryUrl,oneMinuteAgo)) { Result result = fetchSonarStatus(queryUrl); if(null != result && null != result.getStatus()) { return result; }else{ log.debug("Sleeping while waiting for sonar to process job."); } } else { log.debug(String.format("Query url not available yet: %s", queryUrl)); } try { Thread.sleep(SONAR_PROCESSING_WAIT_TIME); } catch (InterruptedException e) { // Do nothing } } while (!waitUntil.isBeforeNow()); String message = String.format("Timed out while waiting for Sonar. Waited %d seconds. This time can be extended " + "using the \"waitForProcessingSeconds\" configuration parameter.", waitForProcessingSeconds); throw new SonarBreakException(message); } /** * Pings a HTTP URL. This effectively sends a request and returns <code>true</code> * if the response code is in the time range of 1 minute back till now. * * @param url The HTTP URL to be pinged. * @return <code>true</code> if the given HTTP URL has returned response code 200-399 on a HEAD request, * otherwise <code>false</code>. * @throws IOException If the sonar server is not available */ protected boolean isAnalysisAvailable(URL url, DateTime oneMinuteAgo) throws SonarBreakException,IOException { InputStream in = null; try { URLConnection connection = url.openConnection(); connection.setRequestProperty("Accept", "application/json"); in = connection.getInputStream(); String response = IOUtils.toString(in); CeResponse result = parseResponse(response,CeResponse.class); return null != result && oneMinuteAgo.isBefore(result.getAnalysisTime()); } finally { IOUtils.closeQuietly(in); } } /** * Get the status of a build project from sonar. This returns the current status that sonar has and does not * do any checking to ensure it matches the current project * * @param queryURL The sonar URL to hit to get the status * @return The sonar response include quality gate status * @throws IOException * @throws SonarBreakException */ private Result fetchSonarStatus(URL queryURL) throws IOException, SonarBreakException { InputStream in = null; try { URLConnection connection = queryURL.openConnection(); connection.setRequestProperty("Accept", "application/json"); in = connection.getInputStream(); String response = IOUtils.toString(in); return parseResponse(response, Result.class); } finally { IOUtils.closeQuietly(in); } } /** * Pings a HTTP URL. This effectively sends a HEAD request and returns <code>true</code> if the response code is in * the 200-399 range. * * @param url The HTTP URL to be pinged. * @param retryCount How many times to check for sonar before giving up * @return <code>true</code> if the given HTTP URL has returned response code 200-399 on a HEAD request, * otherwise <code>false</code>. * @throws IOException If the sonar server is not available */ protected boolean isURLAvailable(URL url, int retryCount) throws IOException { boolean serviceFound = false; for (int i = 0; i < retryCount; i++) { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("HEAD"); int responseCode = connection.getResponseCode(); if (200 <= responseCode && responseCode <= 399) { log.debug(String.format("Got a valid response of %d from %s", responseCode, url)); serviceFound = true; break; } else if (i + 1 < retryCount) { // only sleep if not on last iteration try { log.debug("Sleeping while waiting for sonar to become available"); Thread.sleep(2000); } catch (InterruptedException e) { // do nothing } } } return serviceFound; } /** * Parses the string response from sonar into POJOs. * * @param response The json response from the sonar server. * @return Object representing the Sonar response * @throws SonarBreakException Thrown if the response is not JSON or it does not contain quality gate data. */ protected static <T> T parseResponse(String response, Class<T> clazz) throws SonarBreakException { ObjectMapper mapper = new ObjectMapper(); final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); mapper.setDateFormat(df); T result; try { result = mapper.readValue(response, clazz); } catch (IOException e) { String msg = String.format("Unable to parse resp into %s. Json is: %s", clazz.getName() ,response); throw new SonarBreakException(msg, e); } return result; } }