/*
* Copyright (c) 2014 Jacob Schoen
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.plugins;
import hudson.Launcher;
import hudson.Extension;
import hudson.FilePath;
import hudson.util.FormValidation;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.AbstractProject;
import hudson.model.Result;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.plugins.gitlab.Gitlab;
import jenkins.plugins.sonarparser.SonarReportParser;
import jenkins.plugins.sonarparser.models.SonarIssue;
import jenkins.plugins.sonarparser.models.SonarReport;
import net.sf.json.JSONObject;
import org.gitlab.api.models.GitlabMergeRequest;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.QueryParameter;
public class GitlabSonarReporter extends Notifier {
private static final Logger LOGGER = Logger.getLogger(GitlabSonarReporter.class.getName());
private final String projectPath;
private final String sonarResults;
private final Boolean useDefaultMessageHeader;
private final Boolean useDefaultMessageIssue;
private final Boolean useDefaultMessageFooter;
private final String messageHeader;
private final String messageIssue;
private final String messageFooter;
// Fields in config.jelly must match the parameter names in the "DataBoundConstructor"
@DataBoundConstructor
public GitlabSonarReporter(String projectPath, String sonarResults, Boolean useDefaultMessageHeader, Boolean useDefaultMessageIssue, Boolean useDefaultMessageFooter, String messageHeader, String messageIssue, String messageFooter) {
this.projectPath = projectPath;
this.sonarResults = sonarResults;
this.useDefaultMessageHeader = useDefaultMessageHeader;
this.useDefaultMessageIssue = useDefaultMessageIssue;
this.useDefaultMessageFooter = useDefaultMessageFooter;
this.messageHeader = messageHeader;
this.messageIssue = messageIssue;
this.messageFooter = messageFooter;
}
public String getProjectPath() {
return projectPath;
}
public String getSonarResults() {
return sonarResults;
}
@Override
public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
LOGGER.log(Level.INFO, "Starting Gitlab Sonar Reporter, current Build Result: {0}", build.getResult());
Result result = build.getResult();
if(result != null && result.isBetterOrEqualTo(Result.SUCCESS)){
try {
LOGGER.log(Level.INFO, "Looking for Merge Request on Gitlab");
Map variables = build.getBuildVariables();
String mrId = (String)variables.get("gitlabMergeRequestId");
//get the merge request
GitlabMergeRequest mergeRequest = Gitlab.getMergeRequest(this.projectPath, Integer.parseInt(mrId));
LOGGER.log(Level.INFO, "Found Merge Request on Gitlab");
//get the report results
FilePath workspace = build.getWorkspace();
if(workspace != null){
LOGGER.log(Level.INFO, "Getting the Sonar Report.");
SonarReport report = getReport(workspace.absolutize());
//post the comments
postComments(mergeRequest, report);
}
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, null, ex);
} catch (InterruptedException ex) {
LOGGER.log(Level.SEVERE, null, ex);
} catch (Exception ex) {
//we want to make sure we never break the build
LOGGER.log(Level.SEVERE, null, ex);
}
}
return true;
}
private SonarReport getReport(FilePath workspace) throws IOException{
FilePath resultsPath = new FilePath(workspace, this.sonarResults);
InputStream resultsStream = null;
try {
resultsStream = resultsPath.read();
SonarReport report = SonarReportParser.parse(resultsStream);
return report;
} finally {
if(resultsStream != null){
resultsStream.close();
}
}
}
private void postComments(GitlabMergeRequest mergeRequest, SonarReport report){
//we just care about the new issues
List<SonarIssue> newIssues = report.getNewIssues();
LOGGER.log(Level.INFO, "Number of new issues: {0}", newIssues.size());
String comment = "";
for (SonarIssue issue : newIssues){
if(comment.length() > 0){
//we need to a few lines
comment = comment + " \n";
}
comment = comment + issueMarkup(issue);
}
comment = headerFooterMarkup(report, getMessageHeader())
+ comment
+ headerFooterMarkup(report, getMessageFooter());
LOGGER.log(Level.INFO, "Creating note on Gitlab.");
Gitlab.createNote(mergeRequest, comment);
}
public String getMessageHeader(){
if(this.useDefaultMessageHeader){
return getDescriptor().getMessageHeader();
}
return this.messageHeader;
}
public String getMessageFooter(){
if(this.useDefaultMessageFooter){
return getDescriptor().getMessageFooter();
}
return this.messageFooter;
}
public String getMessageIssue(){
if(this.useDefaultMessageIssue){
return getDescriptor().getMessageIssue();
}
return this.messageIssue;
}
private String headerFooterMarkup(SonarReport report, String template){
String message = template;
message = message.replaceAll("\\$NEW_ISSUE_COUNT", ""+report.getNewIssues().size());
message = message.replaceAll("\\$TOTAL_ISSUE_COUNT", ""+report.getIssues().size());
return message;
}
private String issueMarkup(SonarIssue issue){
String message = getMessageIssue();
message = message.replaceAll("\\$KEY", issue.getSeverity());
message = message.replaceAll("\\$COMPONENT", issue.getComponent());
message = message.replaceAll("\\$LINE", ""+issue.getLine());
message = message.replaceAll("\\$MESSAGE", issue.getMessage());
message = message.replaceAll("\\$SEVERITY", issue.getSeverity());
message = message.replaceAll("\\$RULE", issue.getRule());
return message;
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl) super.getDescriptor();
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@Extension
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* Descriptor for {@link GitlabSonarReporter}. Used as a singleton. The
* class is marked as public so that it can be accessed from views.
*
* <p>
* See
* <tt>src/main/resources/hudson/plugins/hello_world/GitlabSonarReporter/*.jelly</tt>
* for the actual HTML fragment for the configuration screen.
*/
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
private String botUsername = "jenkins";
private String gitlabHostUrl;
private String botApiToken;
private boolean ignoreCertificateErrors = false;
private String messageHeader;
private String messageIssue;
private String messageFooter;
public DescriptorImpl() {
load();
}
@Override
public boolean isApplicable(Class<? extends AbstractProject> type) {
return true;
}
@Override
public String getDisplayName() {
return "Gitlab Merge Request Sonar Results Poster";
}
@Override
public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
botUsername = formData.getString("botUsername");
botApiToken = formData.getString("botApiToken");
gitlabHostUrl = formData.getString("gitlabHostUrl");
ignoreCertificateErrors = formData.getBoolean("ignoreCertificateErrors");
messageHeader = formData.getString("messageHeader");
messageIssue = formData.getString("messageIssue");
messageFooter = formData.getString("messageFooter");
save();
return super.configure(req, formData);
}
public FormValidation doCheckGitlabHostUrl(@QueryParameter String value) {
if (!value.isEmpty()) {
return FormValidation.ok();
}
return FormValidation.error("Gitlab Host Url needs to be set");
}
public FormValidation doCheckBotUsername(@QueryParameter String value) {
if (value == null || value.isEmpty()) {
return FormValidation.error("You must provide a username for the Jenkins user");
}
return FormValidation.ok();
}
public FormValidation doCheckBotApiToken(@QueryParameter String value) {
if (value == null || value.isEmpty()) {
return FormValidation.error("You must provide an API token for the Jenkins user");
}
return FormValidation.ok();
}
public boolean isIgnoreCertificateErrors() {
return ignoreCertificateErrors;
}
public String getBotApiToken() {
return botApiToken;
}
public String getGitlabHostUrl() {
return gitlabHostUrl;
}
public String getBotUsername() {
return botUsername;
}
public String getMessageHeader() {
return messageHeader;
}
public String getMessageIssue() {
return messageIssue;
}
public String getMessageFooter() {
return messageFooter;
}
}
}