diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java index d9a679562..fc49b1a2f 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketApi.java @@ -111,10 +111,8 @@ public interface BitbucketApi { /** * Register a webhook on the repository. - * - * @param hook the webhook object */ - void registerCommitWebHook(BitbucketWebHook hook); + void registerCommitWebHook(); /** * Remove the webhook (ID field required) from the repository. diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java index 96c5f4aed..44ea544aa 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClient.java @@ -30,11 +30,14 @@ import java.net.Proxy; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.HookEventType; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpException; @@ -270,7 +273,7 @@ public BitbucketCommit resolveCommit(String hash) { @Override @CheckForNull public String resolveSourceFullHash(BitbucketPullRequest pull) { - String response = getRequest(V2_API_BASE_URL + pull.getSource().getRepository().getOwnerName() + "/" + + String response = getRequest(V2_API_BASE_URL + pull.getSource().getRepository().getOwnerName() + "/" + pull.getSource().getRepository().getRepositoryName() + "/commit/" + pull.getSource().getCommit().getHash()); try { return parse(response, BitbucketCloudCommit.class).getHash(); @@ -282,14 +285,24 @@ public String resolveSourceFullHash(BitbucketPullRequest pull) { /** {@inheritDoc} */ @Override - public void registerCommitWebHook(BitbucketWebHook hook) { + public void registerCommitWebHook() { try { - postRequest(V2_API_BASE_URL + owner + "/" + repositoryName + "/hooks", asJson(hook)); + postRequest(V2_API_BASE_URL + owner + "/" + repositoryName + "/hooks", asJson(getHook())); } catch (IOException e) { LOGGER.log(Level.SEVERE, "cannot register webhook", e); } } + private BitbucketWebHook getHook() { + BitbucketRepositoryHook hooks = new BitbucketRepositoryHook(); + hooks.setActive(true); + hooks.setDescription("Jenkins hooks"); + hooks.setUrl(Jenkins.getActiveInstance().getRootUrl() + BitbucketSCMSourcePushHookReceiver.FULL_PATH); + hooks.setEvents(Arrays.asList(HookEventType.PUSH.getKey(), + HookEventType.PULL_REQUEST_CREATED.getKey(), HookEventType.PULL_REQUEST_UPDATED.getKey())); + return hooks; + } + /** {@inheritDoc} */ @Override public void removeCommitWebHook(BitbucketWebHook hook) { diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java index cc50f0611..8c4d65df0 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/hooks/WebhookAutoRegisterListener.java @@ -106,7 +106,7 @@ public void doRun() { // synchronized just to avoid duplicated webhooks in case SCMSourceOwner is updated repeteadly and quickly private synchronized void registerHooks(SCMSourceOwner owner) { - List sources = getBitucketSCMSources(owner); + List sources = getBitbucketSCMSources(owner); for (BitbucketSCMSource source : sources) { if (source.isAutoRegisterHook()) { BitbucketApi bitbucket = source.buildBitbucketClient(); @@ -123,7 +123,7 @@ private synchronized void registerHooks(SCMSourceOwner owner) { String rootUrl = Jenkins.getActiveInstance().getRootUrl(); if (rootUrl != null && !rootUrl.startsWith("http://localhost")) { LOGGER.info(String.format("Registering hook for %s/%s", source.getRepoOwner(), source.getRepository())); - bitbucket.registerCommitWebHook(getHook()); + bitbucket.registerCommitWebHook(); } else { LOGGER.warning(String.format("Can not register hook. Jenkins root URL is not valid: %s", rootUrl)); } @@ -133,7 +133,7 @@ private synchronized void registerHooks(SCMSourceOwner owner) { } private void removeHooks(SCMSourceOwner owner) { - List sources = getBitucketSCMSources(owner); + List sources = getBitbucketSCMSources(owner); for (BitbucketSCMSource source : sources) { if (source.isAutoRegisterHook()) { BitbucketApi bitbucket = source.buildBitbucketClient(); @@ -150,7 +150,7 @@ private void removeHooks(SCMSourceOwner owner) { LOGGER.info(String.format("Removing hook for %s/%s", source.getRepoOwner(), source.getRepository())); bitbucket.removeCommitWebHook(hook); } else { - LOGGER.log(Level.FINE, String.format("NOT removing hook for %s/%s because does not exists or its used in other project", + LOGGER.log(Level.FINE, String.format("NOT removing hook for %s/%s because does not exists or its used in other project", source.getRepoOwner(), source.getRepository())); } } @@ -173,7 +173,7 @@ private boolean isUsedSomewhereElse(SCMSourceOwner owner, String repoOwner, Stri return false; } - private List getBitucketSCMSources(SCMSourceOwner owner) { + private List getBitbucketSCMSources(SCMSourceOwner owner) { List sources = new ArrayList(); for (SCMSource source : owner.getSCMSources()) { if (source instanceof BitbucketSCMSource) { @@ -183,17 +183,6 @@ private List getBitucketSCMSources(SCMSourceOwner owner) { return sources; } - private BitbucketWebHook getHook() { - // TODO: generalize this for BB server - BitbucketRepositoryHook hooks = new BitbucketRepositoryHook(); - hooks.setActive(true); - hooks.setDescription("Jenkins hooks"); - hooks.setUrl(Jenkins.getActiveInstance().getRootUrl() + BitbucketSCMSourcePushHookReceiver.FULL_PATH); - hooks.setEvents(Arrays.asList(HookEventType.PUSH.getKey(), - HookEventType.PULL_REQUEST_CREATED.getKey(), HookEventType.PULL_REQUEST_UPDATED.getKey())); - return hooks; - } - /** * We need a single thread executor to run webhooks operations in background but in order. * Registrations and removals need to be done in the same order than they were called by the item listener. diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java index 43534859b..f0ef06eff 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerAPIClient.java @@ -23,55 +23,37 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.server.client; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpException; -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.HttpMethod; -import org.apache.commons.httpclient.NameValuePair; -import org.apache.commons.httpclient.UsernamePasswordCredentials; -import org.apache.commons.httpclient.URIException; -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.StringRequestEntity; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.codehaus.jackson.map.ObjectMapper; - -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketBuildStatus; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCommit; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketPullRequest; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; +import com.cloudbees.jenkins.plugins.bitbucket.api.*; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; +import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranch; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerBranches; import com.cloudbees.jenkins.plugins.bitbucket.server.client.branch.BitbucketServerCommit; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequest; import com.cloudbees.jenkins.plugins.bitbucket.server.client.pullrequest.BitbucketServerPullRequests; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerProject; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepositories; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerRepository; +import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.*; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; - import hudson.ProxyConfiguration; import hudson.util.Secret; import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.*; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.codehaus.jackson.map.ObjectMapper; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Bitbucket API client. @@ -92,18 +74,21 @@ public class BitbucketServerAPIClient implements BitbucketApi { private static final String API_PROJECT_PATH = API_BASE_PATH + "/projects/%s"; private static final String API_COMMIT_COMMENT_PATH = API_REPOSITORY_PATH + "/commits/%s/comments"; + private static final String WEBHOOK_BASE_PATH = "/rest/webhook/1.0"; + private static final String WEBHOOK_REPOSITORY_PATH = WEBHOOK_BASE_PATH + "/projects/%s/repos/%s/configurations"; + private static final String WEBHOOK_REPOSITORY_CONFIG_PATH = WEBHOOK_REPOSITORY_PATH + "/%s"; + private static final String API_COMMIT_STATUS_PATH = "/rest/build-status/1.0/commits/%s"; private static final int MAX_PAGES = 100; /** - * Repository owner. - * This must be null if {@link #project} is not null. + * Repository owner or project name. */ private String owner; /** - * Thre repository that this object is managing. + * The repository that this object is managing. */ private String repositoryName; @@ -114,7 +99,7 @@ public class BitbucketServerAPIClient implements BitbucketApi { /** * Credentials to access API services. - * Almost @NonNull (but null is accepted for annonymous access). + * Almost @NonNull (but null is accepted for anonymous access). */ private UsernamePasswordCredentials credentials; @@ -157,9 +142,9 @@ public String getOwner() { * In Bitbucket server the top level entity is the Project, but the JSON API accepts users as a replacement * of Projects in most of the URLs (it's called user centric API). * - * This method returns the appropiate string to be placed in request URLs taking into account if this client + * This method returns the appropriate string to be placed in request URLs taking into account if this client * object was created as a user centric instance or not. - * + * * @return the ~user or project */ public String getUserCentricOwner() { @@ -178,7 +163,7 @@ public List getPullRequests() { String url = String.format(API_PULL_REQUESTS_PATH, getUserCentricOwner(), repositoryName, 0); try { - List pullRequests = new ArrayList(); + List pullRequests = new ArrayList<>(); Integer pageNumber = 1; String response = getRequest(url); BitbucketServerPullRequests page = parse(response, BitbucketServerPullRequests.class); @@ -193,7 +178,7 @@ public List getPullRequests() { } catch (IOException e) { LOGGER.log(Level.SEVERE, "invalid pull requests response", e); } - return Collections.EMPTY_LIST; + return Collections.emptyList(); } /** {@inheritDoc} */ @@ -269,7 +254,7 @@ public List getBranches() { String url = String.format(API_BRANCHES_PATH, getUserCentricOwner(), repositoryName, 0); try { - List branches = new ArrayList(); + List branches = new ArrayList<>(); Integer pageNumber = 1; String response = getRequest(url); BitbucketServerBranches page = parse(response, BitbucketServerBranches.class); @@ -284,7 +269,7 @@ public List getBranches() { } catch (IOException e) { LOGGER.log(Level.SEVERE, "invalid branches response", e); } - return Collections.EMPTY_LIST; + return Collections.emptyList(); } /** {@inheritDoc} */ @@ -306,19 +291,35 @@ public String resolveSourceFullHash(BitbucketPullRequest pull) { } @Override - public void registerCommitWebHook(BitbucketWebHook hook) { - // TODO + public void registerCommitWebHook() { + try { + putRequest(String.format(WEBHOOK_REPOSITORY_PATH, getUserCentricOwner(), repositoryName), serialize(getHook())); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "cannot register webhook", e); + } + } + + private BitbucketWebHook getHook() { + BitbucketServerWebhook hooks = new BitbucketServerWebhook(); + hooks.setActive(true); + hooks.setDescription("Jenkins hooks"); + hooks.setUrl(Jenkins.getActiveInstance().getRootUrl() + BitbucketSCMSourcePushHookReceiver.FULL_PATH); + return hooks; } @Override public void removeCommitWebHook(BitbucketWebHook hook) { - // TODO + deleteRequest(String.format(WEBHOOK_REPOSITORY_CONFIG_PATH, getUserCentricOwner(), repositoryName, hook.getUuid())); } @Override - public List getWebHooks() { - // TODO - return Collections.EMPTY_LIST; + public List getWebHooks() { + String response = getRequest(String.format(WEBHOOK_REPOSITORY_PATH, getUserCentricOwner(), repositoryName)); + try { + return parse(response, BitbucketServerWebhooks.class); + } catch (IOException e) { + return Collections.emptyList(); + } } /** @@ -347,7 +348,7 @@ public List getRepositories(UserRoleInRepository role String url = String.format(API_REPOSITORIES_PATH, getUserCentricOwner(), 0); try { - List repositories = new ArrayList(); + List repositories = new ArrayList<>(); Integer pageNumber = 1; String response = getRequest(url); BitbucketServerRepositories page = parse(response, BitbucketServerRepositories.class); @@ -362,7 +363,7 @@ public List getRepositories(UserRoleInRepository role } catch (IOException e) { LOGGER.log(Level.SEVERE, "invalid branches response", e); } - return Collections.EMPTY_LIST; + return Collections.emptyList(); } /** {@inheritDoc} */ @@ -374,7 +375,7 @@ public List getRepositories() { @Override public boolean isPrivate() { BitbucketRepository repo = getRepository(); - return repo != null ? repo.isPrivate() : false; + return repo != null && repo.isPrivate(); } @@ -397,8 +398,6 @@ private String getRequest(String path) { if (httpget.getStatusCode() != HttpStatus.SC_OK) { throw new BitbucketRequestException(httpget.getStatusCode(), "HTTP request error. Status: " + httpget.getStatusCode() + ": " + httpget.getStatusText() + ".\n" + response); } - } catch (HttpException e) { - throw new BitbucketRequestException(0, "Communication error: " + e, e); } catch (IOException e) { throw new BitbucketRequestException(0, "Communication error: " + e, e); } finally { @@ -436,7 +435,7 @@ private static void setClientProxyParams(String host, HttpClient client) { } if (proxy.type() != Proxy.Type.DIRECT) { - final InetSocketAddress proxyAddress = (InetSocketAddress)proxy.address(); + final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); LOGGER.fine("Jenkins proxy: " + proxy.address()); client.getHostConfiguration().setProxy(proxyAddress.getHostString(), proxyAddress.getPort()); String username = proxyConfig.getUserName(); @@ -444,7 +443,7 @@ private static void setClientProxyParams(String host, HttpClient client) { if (username != null && !"".equals(username.trim())) { LOGGER.fine("Using proxy authentication (user=" + username + ")"); client.getState().setProxyCredentials(AuthScope.ANY, - new UsernamePasswordCredentials(username, password)); + new UsernamePasswordCredentials(username, password)); } } } @@ -457,8 +456,6 @@ private int getRequestStatus(String path) { try { client.executeMethod(httpget); return httpget.getStatusCode(); - } catch (HttpException e) { - LOGGER.log(Level.SEVERE, "Communication error", e); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Communication error", e); } finally { @@ -483,13 +480,24 @@ private String serialize(T o) throws IOException { private String postRequest(String path, NameValuePair[] params) throws UnsupportedEncodingException { PostMethod httppost = new PostMethod(this.baseURL + path); httppost.setRequestEntity(new StringRequestEntity(nameValueToJson(params), "application/json", "UTF-8")); - return postRequest(httppost); + return doRequest(httppost); } private String postRequest(String path, String content) throws UnsupportedEncodingException { PostMethod httppost = new PostMethod(this.baseURL + path); httppost.setRequestEntity(new StringRequestEntity(content, "application/json", "UTF-8")); - return postRequest(httppost); + return doRequest(httppost); + } + + private String putRequest(String path, String content) throws UnsupportedEncodingException { + PutMethod httppost = new PutMethod(this.baseURL + path); + httppost.setRequestEntity(new StringRequestEntity(content, "application/json", "UTF-8")); + return doRequest(httppost); + } + + private String deleteRequest(String path) { + DeleteMethod httpDelete = new DeleteMethod(this.baseURL + path); + return doRequest(httpDelete); } private String nameValueToJson(NameValuePair[] params) { @@ -500,31 +508,29 @@ private String nameValueToJson(NameValuePair[] params) { return o.toString(); } - private String postRequest(PostMethod httppost) throws UnsupportedEncodingException { - HttpClient client = getHttpClient(getMethodHost(httppost)); + private String doRequest(HttpMethod httpMethod) { + HttpClient client = getHttpClient(getMethodHost(httpMethod)); client.getState().setCredentials(AuthScope.ANY, credentials); client.getParams().setAuthenticationPreemptive(true); String response = null; InputStream responseBodyAsStream = null; try { - client.executeMethod(httppost); - if (httppost.getStatusCode() == HttpStatus.SC_NO_CONTENT) { + client.executeMethod(httpMethod); + if (httpMethod.getStatusCode() == HttpStatus.SC_NO_CONTENT) { // 204, no content return ""; } - responseBodyAsStream = httppost.getResponseBodyAsStream(); + responseBodyAsStream = httpMethod.getResponseBodyAsStream(); if (responseBodyAsStream != null) { response = IOUtils.toString(responseBodyAsStream, "UTF-8"); } - if (httppost.getStatusCode() != HttpStatus.SC_OK && httppost.getStatusCode() != HttpStatus.SC_CREATED) { - throw new BitbucketRequestException(httppost.getStatusCode(), "HTTP request error. Status: " + httppost.getStatusCode() + ": " + httppost.getStatusText() + ".\n" + response); + if (httpMethod.getStatusCode() != HttpStatus.SC_OK && httpMethod.getStatusCode() != HttpStatus.SC_CREATED) { + throw new BitbucketRequestException(httpMethod.getStatusCode(), "HTTP request error. Status: " + httpMethod.getStatusCode() + ": " + httpMethod.getStatusText() + ".\n" + response); } - } catch (HttpException e) { - throw new BitbucketRequestException(0, "Communication error: " + e, e); } catch (IOException e) { throw new BitbucketRequestException(0, "Communication error: " + e, e); } finally { - httppost.releaseConnection(); + httpMethod.releaseConnection(); if (responseBodyAsStream != null) { IOUtils.closeQuietly(responseBodyAsStream); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhook.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhook.java new file mode 100644 index 000000000..b805b8de3 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhook.java @@ -0,0 +1,64 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.client.repository; + + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; +import org.codehaus.jackson.annotate.JsonIgnore; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +import java.util.Collections; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BitbucketServerWebhook implements BitbucketWebHook { + @JsonProperty("id") + private Integer uid; + @JsonProperty("title") + private String description; + @JsonProperty("url") + private String url; + @JsonProperty("enabled") + private boolean active; + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + @Override + @JsonIgnore + public List getEvents() { + return Collections.emptyList(); + } + + @Override + @JsonIgnore + public String getUuid() { + if (uid != null) { + return String.valueOf(uid); + } + return null; + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhooks.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhooks.java new file mode 100644 index 000000000..bed8d999e --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerWebhooks.java @@ -0,0 +1,6 @@ +package com.cloudbees.jenkins.plugins.bitbucket.server.client.repository; + +import java.util.ArrayList; + +public class BitbucketServerWebhooks extends ArrayList { +}