aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/jenkinsci/plugins
diff options
context:
space:
mode:
authorIgor Pashev <pashev.igor@gmail.com>2017-12-05 11:37:56 +0300
committerIgor Pashev <pashev.igor@gmail.com>2017-12-13 21:39:09 +0300
commit5fa1952a2e582f2c428584c5ccc1800132559df0 (patch)
tree7446f557a3bc5448fd48292a327eae7f0f3afa19 /src/main/java/org/jenkinsci/plugins
parent92cf04a50b051cb6c96d0000eb8763797b239496 (diff)
downloadbbprb-5fa1952a2e582f2c428584c5ccc1800132559df0.tar.gz
Version 0.1.00.1.0
Diffstat (limited to 'src/main/java/org/jenkinsci/plugins')
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java39
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java69
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java303
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java80
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java140
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java246
-rw-r--r--src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java9
7 files changed, 886 insertions, 0 deletions
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java
new file mode 100644
index 0000000..2e9d8bc
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketAdditionalParameterEnvironmentContributor.java
@@ -0,0 +1,39 @@
+package org.jenkinsci.plugins.bbprb;
+
+import hudson.EnvVars;
+import hudson.Extension;
+import hudson.model.*;
+
+import java.io.IOException;
+
+@Extension
+public class BitbucketAdditionalParameterEnvironmentContributor
+ extends EnvironmentContributor {
+ @Override
+ public void buildEnvironmentFor(Run run, EnvVars envVars,
+ TaskListener taskListener)
+ throws IOException, InterruptedException {
+
+ BitbucketCause cause = (BitbucketCause)run.getCause(BitbucketCause.class);
+ if (cause == null) {
+ return;
+ }
+
+ putEnvVar(envVars, "destinationRepository",
+ cause.getDestinationRepository());
+ putEnvVar(envVars, "pullRequestAuthor", cause.getPullRequestAuthor());
+ putEnvVar(envVars, "pullRequestId", cause.getPullRequestId());
+ putEnvVar(envVars, "pullRequestTitle", cause.getPullRequestTitle());
+ putEnvVar(envVars, "sourceBranch", cause.getSourceBranch());
+ putEnvVar(envVars, "sourceRepository", cause.getSourceRepository());
+ putEnvVar(envVars, "targetBranch", cause.getTargetBranch());
+ }
+
+ private static void putEnvVar(EnvVars envs, String name, String value) {
+ envs.put(name, getString(value, ""));
+ }
+
+ private static String getString(String actual, String d) {
+ return actual == null ? d : actual;
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java
new file mode 100644
index 0000000..efcc13a
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildListener.java
@@ -0,0 +1,69 @@
+package org.jenkinsci.plugins.bbprb;
+
+import hudson.Extension;
+import hudson.model.AbstractBuild;
+import hudson.model.Job;
+import hudson.model.TaskListener;
+import hudson.model.listeners.RunListener;
+import hudson.triggers.Trigger;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import hudson.model.Result;
+
+import org.jenkinsci.plugins.bbprb.bitbucket.BuildState;
+
+@Extension
+public class BitbucketBuildListener extends RunListener<AbstractBuild<?, ?>> {
+
+ @Override
+ public void onStarted(AbstractBuild<?, ?> build, TaskListener listener) {
+ BitbucketCause cause = build.getCause(BitbucketCause.class);
+ if (cause == null) {
+ return;
+ }
+
+ BitbucketBuildTrigger trigger = extractTrigger(build);
+ if (trigger == null) {
+ return;
+ }
+
+ LOGGER.log(Level.FINE, "Started by BitbucketBuildTrigger");
+ trigger.setPRState(cause, BuildState.INPROGRESS, build.getUrl());
+ try {
+ build.setDescription(
+ build.getCause(BitbucketCause.class).getShortDescription());
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Could not set build description: {0}",
+ e.getMessage());
+ }
+ }
+
+ @Override
+ public void onCompleted(AbstractBuild<?, ?> build, TaskListener listener) {
+ BitbucketBuildTrigger trigger = extractTrigger(build);
+ if (trigger != null) {
+ LOGGER.log(Level.FINE, "Completed after BitbucketBuildTrigger");
+ Result result = build.getResult();
+ BuildState state = (result == Result.SUCCESS) ? BuildState.SUCCESSFUL
+ : BuildState.FAILED;
+ BitbucketCause cause = build.getCause(BitbucketCause.class);
+ trigger.setPRState(cause, state, build.getUrl());
+ }
+ }
+
+ private static BitbucketBuildTrigger
+ extractTrigger(AbstractBuild<?, ?> build) {
+ BitbucketBuildTrigger trigger =
+ build.getProject().getTrigger(BitbucketBuildTrigger.class);
+
+ if ((trigger != null) && (trigger instanceof BitbucketBuildTrigger)) {
+ return trigger;
+ } else {
+ return null;
+ }
+ }
+
+ private static final Logger LOGGER =
+ Logger.getLogger(BitbucketBuildListener.class.getName());
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java
new file mode 100644
index 0000000..b0c6ece
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketBuildTrigger.java
@@ -0,0 +1,303 @@
+package org.jenkinsci.plugins.bbprb;
+
+import antlr.ANTLRException;
+import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
+import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
+import com.cloudbees.plugins.credentials.CredentialsMatchers;
+import com.cloudbees.plugins.credentials.CredentialsProvider;
+import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
+import hudson.Extension;
+import hudson.model.AbstractProject;
+import hudson.model.Cause;
+import hudson.model.Executor;
+import hudson.model.Item;
+import hudson.model.ParameterDefinition;
+import hudson.model.ParametersAction;
+import hudson.model.ParametersDefinitionProperty;
+import hudson.model.ParameterValue;
+import hudson.model.queue.QueueTaskFuture;
+import hudson.model.Queue;
+import hudson.model.Result;
+import hudson.model.Run;
+import hudson.plugins.git.RevisionParameterAction;
+import hudson.security.ACL;
+import hudson.triggers.Trigger;
+import hudson.triggers.TriggerDescriptor;
+import hudson.util.ListBoxModel;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import jenkins.model.Jenkins;
+import jenkins.model.ParameterizedJobMixIn;
+import net.sf.json.JSONObject;
+import org.acegisecurity.context.SecurityContext;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.apache.commons.lang.StringUtils;
+import org.jenkinsci.Symbol;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.StaplerRequest;
+import static com.cloudbees.plugins.credentials.CredentialsMatchers.instanceOf;
+
+import org.jenkinsci.plugins.bbprb.bitbucket.ApiClient;
+import org.jenkinsci.plugins.bbprb.bitbucket.BuildState;
+
+public class BitbucketBuildTrigger extends Trigger<AbstractProject<?, ?>> {
+ private final String ciKey;
+ private final String ciName;
+ private final String credentialsId;
+ private final String destinationRepository;
+ private final boolean cancelOutdatedJobs;
+ private final boolean checkDestinationCommit;
+
+ // XXX: This is for Jelly.
+ // https://wiki.jenkins.io/display/JENKINS/Basic+guide+to+Jelly+usage+in+Jenkins
+ public String getCiKey() {
+ return this.ciKey;
+ }
+ public String getCiName() {
+ return this.ciName;
+ }
+ public String getCredentialsId() {
+ return this.credentialsId;
+ }
+ public String getDestinationRepository() {
+ return this.destinationRepository;
+ }
+ public boolean getCancelOutdatedJobs() {
+ return this.cancelOutdatedJobs;
+ }
+ public boolean getCheckDestinationCommit() {
+ return this.checkDestinationCommit;
+ }
+
+ private transient ApiClient apiClient;
+
+ public static final BitbucketBuildTriggerDescriptor descriptor =
+ new BitbucketBuildTriggerDescriptor();
+
+ @DataBoundConstructor
+ public BitbucketBuildTrigger(String credentialsId,
+ String destinationRepository, String ciKey,
+ String ciName, boolean checkDestinationCommit,
+ boolean cancelOutdatedJobs)
+ throws ANTLRException {
+ super();
+ this.apiClient = null;
+ this.cancelOutdatedJobs = cancelOutdatedJobs;
+ this.checkDestinationCommit = checkDestinationCommit;
+ this.ciKey = ciKey;
+ this.ciName = ciName;
+ this.credentialsId = credentialsId;
+ this.destinationRepository = destinationRepository;
+ }
+
+ @Override
+ public void start(AbstractProject<?, ?> project, boolean newInstance) {
+ logger.log(Level.FINE, "Started for `{0}`", project.getFullName());
+
+ super.start(project, newInstance);
+
+ if (credentialsId != null && !credentialsId.isEmpty()) {
+ logger.log(Level.FINE, "Looking up credentials `{0}`",
+ this.credentialsId);
+ List<UsernamePasswordCredentials> all =
+ CredentialsProvider.lookupCredentials(
+ UsernamePasswordCredentials.class, (Item)null, ACL.SYSTEM,
+ URIRequirementBuilder.fromUri("https://bitbucket.org").build());
+ UsernamePasswordCredentials creds = CredentialsMatchers.firstOrNull(
+ all, CredentialsMatchers.withId(this.credentialsId));
+ if (creds != null) {
+ logger.log(Level.INFO, "Creating Bitbucket API client");
+ this.apiClient = new ApiClient(
+ creds.getUsername(), creds.getPassword().getPlainText(),
+ this.destinationRepository, this.ciKey, this.ciName);
+ } else {
+ logger.log(Level.SEVERE, "Credentials `{0}` not found",
+ this.credentialsId);
+ }
+ } else {
+ logger.log(Level.WARNING, "Missing Bitbucket API credentials");
+ }
+ }
+
+ public void setPRState(BitbucketCause cause, BuildState state, String path) {
+ if (this.apiClient != null) {
+ logger.log(Level.INFO, "Setting status of PR #{0} to {1} for {2}",
+ new Object[] {cause.getPullRequestId(), state,
+ cause.getDestinationRepository()});
+ this.apiClient.setBuildStatus(cause.getSourceCommitHash(), state,
+ getInstance().getRootUrl() + path, null,
+ this.job.getFullName());
+ } else {
+ logger.log(Level.INFO,
+ "Will not set Bitbucket PR build status (not configured)");
+ }
+ }
+
+ private void startJob(BitbucketCause cause) {
+ if (this.cancelOutdatedJobs) {
+ SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
+ cancelPreviousJobsInQueueThatMatch(cause);
+ abortRunningJobsThatMatch(cause);
+ SecurityContextHolder.setContext(orig);
+ }
+
+ setPRState(cause, BuildState.INPROGRESS, this.job.getUrl());
+
+ this.job.scheduleBuild2(
+ 0, cause, new ParametersAction(this.getDefaultParameters()),
+ new RevisionParameterAction(cause.getSourceCommitHash()));
+ }
+
+ private void
+ cancelPreviousJobsInQueueThatMatch(@Nonnull BitbucketCause cause) {
+ logger.log(Level.FINE, "Looking for queued jobs that match PR #{0}",
+ cause.getPullRequestId());
+ Queue queue = getInstance().getQueue();
+
+ for (Queue.Item item : queue.getItems()) {
+ if (hasCauseFromTheSamePullRequest(item.getCauses(), cause)) {
+ logger.fine("Canceling item in queue: " + item);
+ queue.cancel(item);
+ }
+ }
+ }
+
+ private Jenkins getInstance() {
+ final Jenkins instance = Jenkins.getInstance();
+ if (instance == null) {
+ throw new IllegalStateException("Jenkins instance is NULL!");
+ }
+ return instance;
+ }
+
+ private void
+ abortRunningJobsThatMatch(@Nonnull BitbucketCause bitbucketCause) {
+ logger.log(Level.FINE, "Looking for running jobs that match PR #{0}",
+ bitbucketCause.getPullRequestId());
+ for (Object o : job.getBuilds()) {
+ if (o instanceof Run) {
+ Run build = (Run)o;
+ if (build.isBuilding() &&
+ hasCauseFromTheSamePullRequest(build.getCauses(), bitbucketCause)) {
+ logger.fine("Aborting build: " + build + " since PR is outdated");
+ setBuildDescription(build);
+ final Executor executor = build.getExecutor();
+ if (executor == null) {
+ throw new IllegalStateException("Executor can't be NULL");
+ }
+ executor.interrupt(Result.ABORTED);
+ }
+ }
+ }
+ }
+
+ private void setBuildDescription(final Run build) {
+ try {
+ build.setDescription(
+ "Aborting build by `Bitbucket Pullrequest Builder Plugin`: " + build +
+ " since PR is outdated");
+ } catch (IOException e) {
+ logger.warning("Could not set build description: " + e.getMessage());
+ }
+ }
+
+ private boolean
+ hasCauseFromTheSamePullRequest(@Nullable List<Cause> causes,
+ @Nullable BitbucketCause pullRequestCause) {
+ if (causes != null && pullRequestCause != null) {
+ for (Cause cause : causes) {
+ if (cause instanceof BitbucketCause) {
+ BitbucketCause sc = (BitbucketCause)cause;
+ if (StringUtils.equals(sc.getPullRequestId(),
+ pullRequestCause.getPullRequestId()) &&
+ StringUtils.equals(sc.getSourceRepository(),
+ pullRequestCause.getSourceRepository())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private ArrayList<ParameterValue> getDefaultParameters() {
+ Map<String, ParameterValue> values = new HashMap<String, ParameterValue>();
+ ParametersDefinitionProperty definitionProperty =
+ this.job.getProperty(ParametersDefinitionProperty.class);
+
+ if (definitionProperty != null) {
+ for (ParameterDefinition definition :
+ definitionProperty.getParameterDefinitions()) {
+ values.put(definition.getName(), definition.getDefaultParameterValue());
+ }
+ }
+ return new ArrayList<ParameterValue>(values.values());
+ }
+
+ public void handlePR(JSONObject pr) {
+ JSONObject src = pr.getJSONObject("source");
+ JSONObject dst = pr.getJSONObject("destination");
+ String dstRepository =
+ dst.getJSONObject("repository").getString("full_name");
+ BitbucketCause cause = new BitbucketCause(
+ src.getJSONObject("branch").getString("name"),
+ dst.getJSONObject("branch").getString("name"),
+ src.getJSONObject("repository").getString("full_name"),
+ pr.getString("id"), // FIXME: it is integer
+ dstRepository, pr.getString("title"),
+ src.getJSONObject("commit").getString("hash"),
+ dst.getJSONObject("commit").getString("hash"),
+ pr.getJSONObject("author").getString("username"));
+ if (!dstRepository.equals(this.destinationRepository)) {
+ logger.log(Level.FINE,
+ "Job `{0}`: repository `{1}` does not match `{2}`. Skipping.",
+ new Object[] {this.job.getFullName(), dstRepository,
+ this.destinationRepository});
+ return;
+ }
+ startJob(cause);
+ }
+
+ @Extension
+ @Symbol("bbprb")
+ public static final class BitbucketBuildTriggerDescriptor
+ extends TriggerDescriptor {
+ public BitbucketBuildTriggerDescriptor() {
+ load();
+ }
+
+ @Override
+ public boolean isApplicable(Item item) {
+ return item instanceof AbstractProject;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Bitbucket Pull Requests Builder";
+ }
+
+ @Override
+ public boolean configure(StaplerRequest req, JSONObject json)
+ throws FormException {
+ save();
+ return super.configure(req, json);
+ }
+
+ public ListBoxModel doFillCredentialsIdItems() {
+ return new StandardListBoxModel().withEmptySelection().withMatching(
+ instanceOf(UsernamePasswordCredentials.class),
+ CredentialsProvider.lookupCredentials(
+ StandardUsernamePasswordCredentials.class));
+ }
+ }
+ private static final Logger logger =
+ Logger.getLogger(BitbucketBuildTrigger.class.getName());
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java
new file mode 100644
index 0000000..a4a20a6
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketCause.java
@@ -0,0 +1,80 @@
+package org.jenkinsci.plugins.bbprb;
+
+import hudson.model.Cause;
+
+/**
+ * Created by nishio
+ */
+public class BitbucketCause extends Cause {
+ private final String sourceBranch;
+ private final String targetBranch;
+ private final String sourceRepository;
+ private final String pullRequestId;
+ private final String destinationRepository;
+ private final String pullRequestTitle;
+ private final String sourceCommitHash;
+ private final String destinationCommitHash;
+ private final String pullRequestAuthor;
+ public static final String BITBUCKET_URL = "https://bitbucket.org/";
+
+ public BitbucketCause(String sourceBranch, String targetBranch,
+ String sourceRepository, String pullRequestId,
+ String destinationRepository, String pullRequestTitle,
+ String sourceCommitHash, String destinationCommitHash,
+ String pullRequestAuthor) {
+ this.sourceBranch = sourceBranch;
+ this.targetBranch = targetBranch;
+ this.sourceRepository = sourceRepository;
+ this.pullRequestId = pullRequestId;
+ this.destinationRepository = destinationRepository;
+ this.pullRequestTitle = pullRequestTitle;
+ this.sourceCommitHash = sourceCommitHash;
+ this.destinationCommitHash = destinationCommitHash;
+ this.pullRequestAuthor = pullRequestAuthor;
+ }
+
+ public String getSourceBranch() {
+ return sourceBranch;
+ }
+ public String getTargetBranch() {
+ return targetBranch;
+ }
+
+ public String getSourceRepository() {
+ return sourceRepository;
+ }
+
+ public String getPullRequestId() {
+ return pullRequestId;
+ }
+
+ public String getDestinationRepository() {
+ return destinationRepository;
+ }
+
+ public String getPullRequestTitle() {
+ return pullRequestTitle;
+ }
+
+ public String getSourceCommitHash() {
+ return sourceCommitHash;
+ }
+
+ public String getDestinationCommitHash() {
+ return destinationCommitHash;
+ }
+
+ @Override
+ public String getShortDescription() {
+ String description =
+ "<a href=\"" + BITBUCKET_URL + this.getDestinationRepository();
+ description += "/pull-request/" + this.getPullRequestId();
+ description += "\">#" + this.getPullRequestId() + " " +
+ this.getPullRequestTitle() + "</a>";
+ return description;
+ }
+
+ public String getPullRequestAuthor() {
+ return this.pullRequestAuthor;
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java
new file mode 100644
index 0000000..18b2688
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/BitbucketHookReceiver.java
@@ -0,0 +1,140 @@
+package org.jenkinsci.plugins.bbprb;
+
+import hudson.Extension;
+import hudson.model.UnprotectedRootAction;
+import hudson.security.ACL;
+import hudson.triggers.Trigger;
+import hudson.triggers.TriggerDescriptor;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import jenkins.model.Jenkins;
+import jenkins.model.ParameterizedJobMixIn.ParameterizedJob;
+import net.sf.json.JSONException;
+import net.sf.json.JSONObject;
+import org.acegisecurity.context.SecurityContext;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.apache.commons.io.IOUtils;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+
+@Extension
+public class BitbucketHookReceiver implements UnprotectedRootAction {
+
+ private static final String BITBUCKET_HOOK_URL = "bbprb-hook";
+ private static final String BITBUCKET_UA = "Bitbucket-Webhooks/2.0";
+
+ public void doIndex(StaplerRequest req, StaplerResponse resp)
+ throws IOException {
+
+ String userAgent = req.getHeader("user-agent");
+ if (!BITBUCKET_UA.equals(userAgent)) {
+ LOGGER.log(Level.WARNING, "Bad user agent: `{0}`, expected `{1}`",
+ new Object[] {userAgent, BITBUCKET_UA});
+ resp.setStatus(StaplerResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ String uri = req.getRequestURI();
+ if (!uri.contains("/" + BITBUCKET_HOOK_URL + "/")) {
+ LOGGER.log(Level.WARNING,
+ "BitBucket hook URI does not contain `/{0}/`: `{1}`",
+ new Object[] {BITBUCKET_HOOK_URL, uri});
+ resp.setStatus(StaplerResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ String event = req.getHeader("x-event-key");
+ if (event == null) {
+ LOGGER.log(Level.WARNING, "Missing the `x-event-key` header");
+ resp.setStatus(StaplerResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ String body = IOUtils.toString(req.getInputStream());
+ if (body.isEmpty()) {
+ LOGGER.log(Level.WARNING, "Received empty request body");
+ resp.setStatus(StaplerResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ String contentType = req.getContentType();
+ if (contentType != null &&
+ contentType.startsWith("application/x-www-form-urlencoded")) {
+ body = URLDecoder.decode(body, "UTF-8");
+ }
+ if (body.startsWith("payload=")) {
+ body = body.substring(8);
+ }
+
+ LOGGER.log(Level.FINE,
+ "Received commit hook notification, key: `{0}`, body: `{1}`",
+ new Object[] {event, body});
+
+ try {
+ JSONObject payload = JSONObject.fromObject(body);
+ if (event.startsWith("pullrequest:")) {
+ JSONObject pr = payload.getJSONObject("pullrequest");
+ String state = pr.getString("state");
+ if (!"OPEN".equals(state)) {
+ LOGGER.log(
+ Level.INFO, "Ignoring closed PR ({0}): #{1} {2}",
+ new Object[] {state, pr.getInt("id"), pr.getString("title")});
+ return;
+ }
+ for (BitbucketBuildTrigger trigger : getBitbucketTriggers()) {
+ trigger.handlePR(pr);
+ }
+ return;
+ }
+ } catch (JSONException e) {
+ LOGGER.log(Level.WARNING, e.getMessage());
+ resp.setStatus(StaplerResponse.SC_BAD_REQUEST);
+ return;
+ }
+ }
+
+ private static List<BitbucketBuildTrigger> getBitbucketTriggers() {
+ List<BitbucketBuildTrigger> bbtriggers = new ArrayList<>();
+
+ SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
+ List<ParameterizedJob> jobs =
+ Jenkins.getInstance().getAllItems(ParameterizedJob.class);
+ SecurityContextHolder.setContext(orig);
+
+ for (ParameterizedJob job : jobs) {
+ String jobName = job.getFullName();
+ LOGGER.log(Level.FINER, "Found job: `{0}`", jobName);
+
+ Map<TriggerDescriptor, Trigger<?>> triggers = job.getTriggers();
+
+ for (Trigger<?> trigger : triggers.values()) {
+ if (trigger instanceof BitbucketBuildTrigger) {
+ LOGGER.log(Level.FINE, "Will consider job: `{0}`", jobName);
+ bbtriggers.add((BitbucketBuildTrigger)trigger);
+ }
+ }
+ }
+
+ return bbtriggers;
+ }
+
+ private static final Logger LOGGER =
+ Logger.getLogger(BitbucketHookReceiver.class.getName());
+
+ public String getIconFileName() {
+ return null;
+ }
+
+ public String getDisplayName() {
+ return null;
+ }
+
+ public String getUrlName() {
+ return BITBUCKET_HOOK_URL;
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java
new file mode 100644
index 0000000..80ae3aa
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/ApiClient.java
@@ -0,0 +1,246 @@
+package org.jenkinsci.plugins.bbprb.bitbucket;
+
+import org.apache.commons.httpclient.*;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.type.TypeFactory;
+import org.codehaus.jackson.type.JavaType;
+import org.codehaus.jackson.type.TypeReference;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jenkins.model.Jenkins;
+import hudson.ProxyConfiguration;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.util.EncodingUtil;
+
+/**
+ * Created by nishio
+ */
+public class ApiClient {
+ private static final Logger logger =
+ Logger.getLogger(ApiClient.class.getName());
+ private static final String V1_API_BASE_URL =
+ "https://bitbucket.org/api/1.0/repositories/";
+ private static final String V2_API_BASE_URL =
+ "https://bitbucket.org/api/2.0/repositories/";
+ private static final String COMPUTED_KEY_FORMAT = "%s-%s";
+ private String repository;
+ private Credentials credentials;
+ private String key;
+ private String name;
+ private HttpClientFactory factory;
+
+ public static final byte MAX_KEY_SIZE_BB_API = 40;
+
+ public static class HttpClientFactory {
+ public static final HttpClientFactory INSTANCE = new HttpClientFactory();
+ private static final int DEFAULT_TIMEOUT = 60000;
+
+ public HttpClient getInstanceHttpClient() {
+ HttpClient client = new HttpClient();
+
+ HttpClientParams params = client.getParams();
+ params.setConnectionManagerTimeout(DEFAULT_TIMEOUT);
+ params.setSoTimeout(DEFAULT_TIMEOUT);
+
+ if (Jenkins.getInstance() == null)
+ return client;
+
+ ProxyConfiguration proxy = getInstance().proxy;
+ if (proxy == null)
+ return client;
+
+ logger.log(Level.FINE, "Jenkins proxy: {0}:{1}",
+ new Object[] {proxy.name, proxy.port});
+ client.getHostConfiguration().setProxy(proxy.name, proxy.port);
+ String username = proxy.getUserName();
+ String password = proxy.getPassword();
+
+ // Consider it to be passed if username specified. Sufficient?
+ if (username != null && !"".equals(username.trim())) {
+ logger.log(Level.FINE, "Using proxy authentication (user={0})",
+ username);
+ client.getState().setProxyCredentials(
+ AuthScope.ANY, new UsernamePasswordCredentials(username, password));
+ }
+
+ return client;
+ }
+
+ private Jenkins getInstance() {
+ final Jenkins instance = Jenkins.getInstance();
+ if (instance == null) {
+ throw new IllegalStateException("Jenkins instance is NULL!");
+ }
+ return instance;
+ }
+ }
+
+ public <T extends HttpClientFactory> ApiClient(String username,
+ String password,
+ String repository, String key,
+ String name) {
+ this.credentials = new UsernamePasswordCredentials(username, password);
+ this.repository = repository;
+ this.key = key;
+ this.name = name;
+ this.factory = HttpClientFactory.INSTANCE;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ private static MessageDigest SHA1 = null;
+
+ /**
+ * Retrun
+ * @param keyExPart
+ * @return key parameter for call BitBucket API
+ */
+ private String computeAPIKey(String keyExPart) {
+ String computedKey =
+ String.format(COMPUTED_KEY_FORMAT, this.key, keyExPart);
+
+ if (computedKey.length() > MAX_KEY_SIZE_BB_API) {
+ try {
+ if (SHA1 == null)
+ SHA1 = MessageDigest.getInstance("SHA1");
+ return new String(
+ Hex.encodeHex(SHA1.digest(computedKey.getBytes("UTF-8"))));
+ } catch (NoSuchAlgorithmException e) {
+ logger.log(Level.WARNING, "Failed to create hash provider", e);
+ } catch (UnsupportedEncodingException e) {
+ logger.log(Level.WARNING, "Failed to create hash provider", e);
+ }
+ }
+ return (computedKey.length() <= MAX_KEY_SIZE_BB_API)
+ ? computedKey
+ : computedKey.substring(0, MAX_KEY_SIZE_BB_API);
+ }
+
+ public String buildStatusKey(String bsKey) {
+ return this.computeAPIKey(bsKey);
+ }
+
+ public boolean hasBuildStatus(String revision, String keyEx) {
+ String url = v2("/commit/" + revision + "/statuses/build/" +
+ this.computeAPIKey(keyEx));
+ String reqBody = get(url);
+ return reqBody != null && reqBody.contains("\"state\"");
+ }
+
+ public void setBuildStatus(String revision, BuildState state, String buildUrl,
+ String comment, String keyEx) {
+ String url = v2("/commit/" + revision + "/statuses/build");
+ String computedKey = this.computeAPIKey(keyEx);
+ NameValuePair[] data = new NameValuePair[] {
+ new NameValuePair("description", comment),
+ new NameValuePair("key", computedKey),
+ new NameValuePair("name", this.name),
+ new NameValuePair("state", state.toString()),
+ new NameValuePair("url", buildUrl),
+ };
+ logger.log(Level.FINE,
+ "POST state {0} to {1} with key {2} with response {3}",
+ new Object[] {state, url, computedKey, post(url, data)});
+ }
+
+ public void deletePullRequestApproval(String pullRequestId) {
+ delete(v2("/pullrequests/" + pullRequestId + "/approve"));
+ }
+
+ public void deletePullRequestComment(String pullRequestId, String commentId) {
+ delete(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId));
+ }
+
+ public void updatePullRequestComment(String pullRequestId, String content,
+ String commentId) {
+ NameValuePair[] data = new NameValuePair[] {
+ new NameValuePair("content", content),
+ };
+ put(v1("/pullrequests/" + pullRequestId + "/comments/" + commentId), data);
+ }
+
+ private HttpClient getHttpClient() {
+ return this.factory.getInstanceHttpClient();
+ }
+
+ private String v1(String path) {
+ return V1_API_BASE_URL + this.repository + path;
+ }
+
+ private String v2(String path) {
+ return V2_API_BASE_URL + this.repository + path;
+ }
+
+ private String get(String path) {
+ return send(new GetMethod(path));
+ }
+
+ private String post(String path, NameValuePair[] data) {
+ PostMethod req = new PostMethod(path);
+ req.setRequestBody(data);
+ req.getParams().setContentCharset("utf-8");
+ return send(req);
+ }
+
+ private void delete(String path) {
+ send(new DeleteMethod(path));
+ }
+
+ private void put(String path, NameValuePair[] data) {
+ PutMethod req = new PutMethod(path);
+ req.setRequestBody(EncodingUtil.formUrlEncode(data, "utf-8"));
+ req.getParams().setContentCharset("utf-8");
+ send(req);
+ }
+
+ private String send(HttpMethodBase req) {
+ HttpClient client = getHttpClient();
+ client.getState().setCredentials(AuthScope.ANY, credentials);
+ client.getParams().setAuthenticationPreemptive(true);
+ try {
+ int statusCode = client.executeMethod(req);
+ if (statusCode != HttpStatus.SC_OK) {
+ logger.log(Level.WARNING, "Response status: " + req.getStatusLine() +
+ " URI: " + req.getURI());
+ } else {
+ return req.getResponseBodyAsString();
+ }
+ } catch (HttpException e) {
+ logger.log(Level.WARNING, "Failed to send request.", e);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Failed to send request.", e);
+ } finally {
+ req.releaseConnection();
+ }
+ return null;
+ }
+
+ private <R> R parse(String response, Class<R> cls) throws IOException {
+ return new ObjectMapper().readValue(response, cls);
+ }
+ private <R> R parse(String response, JavaType type) throws IOException {
+ return new ObjectMapper().readValue(response, type);
+ }
+ private <R> R parse(String response, TypeReference<R> ref)
+ throws IOException {
+ return new ObjectMapper().readValue(response, ref);
+ }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java
new file mode 100644
index 0000000..8fe1cdd
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/bbprb/bitbucket/BuildState.java
@@ -0,0 +1,9 @@
+package org.jenkinsci.plugins.bbprb.bitbucket;
+
+/**
+ * Valid build states for a pull request
+ *
+ * @see
+ * "https://confluence.atlassian.com/bitbucket/buildstatus-resource-779295267.html"
+ */
+public enum BuildState { FAILED, INPROGRESS, SUCCESSFUL }