src/main/java/com/andrewensley/sonarteamsnotifier/extension/PayloadBuilder.java
package com.andrewensley.sonarteamsnotifier.extension;
import static java.lang.String.format;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.sonar.api.ce.posttask.Branch;
import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
import org.sonar.api.ce.posttask.QualityGate;
import org.sonar.api.ce.posttask.QualityGate.Condition;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
/**
* Builds a payload for a WebEx Teams message.
*/
class PayloadBuilder {
/**
* Logger.
*/
private static final Logger LOG = Loggers.get(PayloadBuilder.class);
/**
* Project Analysis.
*/
private PostProjectAnalysisTask.ProjectAnalysis analysis;
/**
* Project URL.
*/
private String projectUrl;
/**
* Whether to send notifications on only failures.
*/
private boolean failOnly;
/**
* Whether the overall QualityGate status is OK or not.
*/
private boolean qualityGateOk;
/**
* The change author to mention on failures.
*/
private String changeAuthor = "";
/**
* The URL of the commit.
*/
private String commitUrl = "";
/**
* Decimal format for percentages.
*/
private DecimalFormat percentageFormat;
/**
* Constructor.
*
* @param analysis The Project's analysis.
* @param projectUrl The URL for the project.
* @param failOnly Whether to alert only on failures or not.
* @param qualityGateOk Whether the overall quality gate status is OK or not.
*/
private PayloadBuilder(
PostProjectAnalysisTask.ProjectAnalysis analysis,
String projectUrl,
boolean failOnly,
boolean qualityGateOk
) {
this.analysis = analysis;
this.projectUrl = projectUrl;
this.failOnly = failOnly;
this.qualityGateOk = qualityGateOk;
// Round percentages to 2 decimal points.
this.percentageFormat = new DecimalFormat();
this.percentageFormat.setMaximumFractionDigits(2);
}
/**
* Static pattern PayloadBuilder constructor.
*
* @param analysis The Project's analysis.
* @param projectUrl The URL for the project.
* @param failOnly Whether to alert only on failures or not.
* @param qualityGateOk Whether the overall quality gate status is OK or not.
*
* @return The PayloadBuilder
*/
static PayloadBuilder of(
PostProjectAnalysisTask.ProjectAnalysis analysis,
String projectUrl,
boolean failOnly,
boolean qualityGateOk
) {
return new PayloadBuilder(analysis, projectUrl, failOnly, qualityGateOk);
}
/**
* Set changeAuthor in chained static builder.
*
* @param email The change author's email.
* @param name The change author's name.
*
* @return The PayloadBuilder
*/
PayloadBuilder changeAuthor(String email, String name) {
if (email != null && !email.isEmpty()) {
this.changeAuthor = "<@personEmail:" + email;
if (name != null && !name.isEmpty()) {
this.changeAuthor += "|" + name;
}
this.changeAuthor += ">";
}
return this;
}
/**
* Set commitUrl in chained static builder.
*
* @param commitUrl The URL for the commit.
*
* @return The PayloadBuilder
*/
PayloadBuilder commitUrl(String commitUrl) {
if (commitUrl != null && !commitUrl.isEmpty()) {
this.commitUrl = commitUrl;
}
return this;
}
/**
* Builds the payload.
*
* @return The payload as a JSON-encoded string.
*/
Payload build() {
assertNotNull(projectUrl, "projectUrl");
assertNotNull(failOnly, "failOnly");
assertNotNull(qualityGateOk, "qualityGateOk");
assertNotNull(analysis, "analysis");
QualityGate qualityGate = analysis.getQualityGate();
StringBuilder message = new StringBuilder();
if (qualityGate != null) {
Optional<Branch> branch = analysis.getBranch();
appendHeader(message, qualityGate, branch);
appendCommit(message);
appendBranch(message, branch);
appendDate(message);
appendConditions(message, qualityGate);
}
Payload payload = new Payload(message.toString());
LOG.info("WebEx Teams message: " + payload.markdown);
return payload;
}
/**
* Appends the header to the message.
*
* @param message The StringBuilder being used to build the message.
* @param qualityGate The QualityGate object.
* @param branch The Branch object.
*/
private void appendHeader(
StringBuilder message,
QualityGate qualityGate,
Optional<Branch> branch
) {
message.append(format(
"### %s **%S** [[%s](%s)]\n\n",
qualityGate.getName(),
qualityGate.getStatus(),
analysis.getProject().getName(),
getProjectBranchUrl(branch)
));
}
/**
* Appends commit information to the message.
*
* @param message The StringBuilder being used to build the message.
*/
@SuppressWarnings("deprecation")
private void appendCommit(StringBuilder message) {
String commit = analysis.getScmRevisionId();
message.append("**Commit**: ");
if (commitUrl.isEmpty()) {
message.append(commit);
} else {
message.append(format("[%s](%s)", commit, commitUrl));
}
if (!changeAuthor.isEmpty() && !qualityGateOk) {
message.append(format(" by %s", changeAuthor));
}
message.append(" \n");
}
/**
* Appends the analysis date to the message.
*
* @param message The StringBuilder being used to build the message.
*/
@SuppressWarnings("deprecation")
private void appendDate(StringBuilder message) {
Date date = analysis.getDate();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
message.append(format("**Date**: %s \n", simpleDateFormat.format(date)));
}
/**
* Appends the branch name to the message.
*
* @param message The StringBuilder being used to build the message.
* @param branch The Branch object.
*/
private void appendBranch(StringBuilder message, Optional<Branch> branch) {
if (branchIsNonMain(branch)) {
//noinspection OptionalGetWithoutIsPresent
message.append(format("**Branch**: %s \n", branch.get().getName().orElse("default")));
}
}
/**
* Gets the URL for the project including the branch, if supplied.
*
* @param branch The branch that was analyzed.
*
* @return The Project URL with optional branch.
*/
private String getProjectBranchUrl(Optional<Branch> branch) {
String projectBranchUrl = projectUrl;
if (branchIsNonMain(branch)) {
//noinspection OptionalGetWithoutIsPresent
projectBranchUrl += format("&branch=%s", branch.get().getName().orElse(""));
}
return projectBranchUrl;
}
/**
* Checks if the given branch is set and is not the main/master/default branch.
*
* @param branch The branch to check.
*
* @return True if the branch is set and is not the main/master/default branch. Otherwise false.
*/
private boolean branchIsNonMain(Optional<Branch> branch) {
return branch.isPresent() && !branch.get().isMain();
}
/**
* Appends Condition statuses to the message.
*
* @param message The StringBuilder being used to build the message.
* @param qualityGate The Quality Gate.
*/
private void appendConditions(StringBuilder message, QualityGate qualityGate) {
List<String> conditions = qualityGate.getConditions()
.stream()
.filter(condition -> !failOnly || notOkOrNoValueCondition(condition))
.map(this::translateCondition)
.collect(Collectors.toList());
for (String condition : conditions) {
message.append(condition);
}
}
/**
* Checks that the condition value is not OK or NO_VALUE. Any other value indicates a failure.
*
* @param condition The condition to be checked.
*
* @return True if the condition is not OK or NO_VALUE. False if it is.
*/
private boolean notOkOrNoValueCondition(Condition condition) {
return !(QualityGate.EvaluationStatus.OK.equals(condition.getStatus())
|| QualityGate.EvaluationStatus.NO_VALUE.equals(condition.getStatus()));
}
/**
* Translates individual conditions to formatted strings.
*
* @param condition The condition to translate.
*
* @return The translated condition.
*/
private String translateCondition(Condition condition) {
if (QualityGate.EvaluationStatus.NO_VALUE.equals(condition.getStatus())) {
// No value for given metric
return format(" * **%s**: %s\n", condition.getMetricKey(), condition.getStatus().name());
} else {
return format(
" * **%s**: %s | %s\n",
condition.getMetricKey(),
condition.getStatus().name(),
getConditionString(condition)
);
}
}
/**
* Gets the condition string when there's more detailed information
* about the quality gate condition.
*
* @param condition The Quality Gate Condition.
*
* @return The condition string.
*/
@SuppressWarnings("deprecation")
private String getConditionString(Condition condition) {
StringBuilder sb = new StringBuilder();
appendConditionValue(condition, sb);
if (condition.getWarningThreshold() != null) {
sb.append(", warning if ");
appendConditionComparisonOperator(condition, sb);
sb.append(condition.getWarningThreshold());
}
if (condition.getErrorThreshold() != null) {
sb.append(", error if ");
appendConditionComparisonOperator(condition, sb);
sb.append(condition.getErrorThreshold());
}
return sb.toString();
}
/**
* Appends a condition's value to a StringBuilder.
*
* @param condition The condition.
* @param sb The StringBuilder.
*/
private void appendConditionValue(Condition condition, StringBuilder sb) {
String value = condition.getValue();
if (value.equals("")) {
sb.append("**-**");
} else {
appendNonEmptyValue(condition, sb, value);
}
}
/**
* Appends a non-empty value to the condition string.
*
* @param condition The condition.
* @param sb The StringBuilder.
* @param value The value.
*/
private void appendNonEmptyValue(Condition condition, StringBuilder sb, String value) {
sb.append("**");
if (conditionValueIsPercentage(condition)) {
appendPercentageValue(sb, value);
} else {
sb.append(value);
}
sb.append("**");
}
/**
* Appends a percentage value to the condition string.
*
* @param sb The StringBuilder.
* @param value The percentage value.
*/
private void appendPercentageValue(StringBuilder sb, String value) {
try {
Double percent = Double.parseDouble(value);
sb.append(percentageFormat.format(percent));
sb.append("%");
} catch (NumberFormatException e) {
LOG.error("Failed to parse [{}] into a Double due to [{}]", value, e.getMessage());
sb.append(value);
}
}
/**
* Appends a condition's comparison operator to a StringBuilder.
*
* @param condition The condition.
* @param sb The StringBuilder.
*/
@SuppressWarnings("deprecation")
private void appendConditionComparisonOperator(Condition condition, StringBuilder sb) {
switch (condition.getOperator()) {
case EQUALS:
sb.append("==");
break;
case NOT_EQUALS:
sb.append("!=");
break;
case GREATER_THAN:
sb.append(">");
break;
case LESS_THAN:
sb.append("<");
break;
default:
break;
}
}
/**
* Checks if a condition's value is a percentage by checking the metric key.
*
* @param condition The condition to check.
*
* @return True if the condition's value is a percentage. False if not.
*/
private boolean conditionValueIsPercentage(Condition condition) {
switch (condition.getMetricKey()) {
case CoreMetrics.NEW_COVERAGE_KEY:
case CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY:
return true;
default:
break;
}
return false;
}
/**
* Asserts that an object is not null. Throws an exception if it is.
*
* @param object The object to check.
* @param objectName The name of the object.
*/
private void assertNotNull(Object object, String objectName) {
if (object == null) {
throw new IllegalArgumentException(
"[Assertion failed] - " + objectName + " argument is required; it must not be null"
);
}
}
}