diff --git a/pom.xml b/pom.xml index 287872b8f..66c6d3e14 100644 --- a/pom.xml +++ b/pom.xml @@ -31,10 +31,12 @@ jenkinsci/bitbucket-branch-source-plugin 2.479 ${jenkins.baseline}.1 - 2.0 + 934.0 true @{project.version} + + 1 diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java index 516b143da..cd98c3ecd 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java @@ -26,6 +26,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCloudWorkspace; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; @@ -33,10 +34,10 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketCloudEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; +import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketTeamAvatarMetadataAction; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentials; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.MirrorListSupplier; import com.cloudbees.jenkins.plugins.bitbucket.server.BitbucketServerWebhookImplementation; -import com.cloudbees.jenkins.plugins.bitbucket.server.client.repository.BitbucketServerProject; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; @@ -323,7 +324,7 @@ public void setPattern(String pattern) { @RestrictedSince("2.2.0") @DataBoundSetter public void setAutoRegisterHooks(boolean autoRegisterHook) { - traits.removeIf(trait -> trait instanceof WebhookRegistrationTrait); + traits.removeIf(WebhookRegistrationTrait.class::isInstance); traits.add(new WebhookRegistrationTrait( autoRegisterHook ? WebhookRegistration.ITEM : WebhookRegistration.DISABLE )); @@ -334,8 +335,8 @@ public void setAutoRegisterHooks(boolean autoRegisterHook) { @RestrictedSince("2.2.0") public boolean isAutoRegisterHooks() { for (SCMTrait> t : traits) { - if (t instanceof WebhookRegistrationTrait) { - return ((WebhookRegistrationTrait) t).getMode() != WebhookRegistration.DISABLE; + if (t instanceof WebhookRegistrationTrait hookTrait) { + return hookTrait.getMode() != WebhookRegistration.DISABLE; } } return true; @@ -348,8 +349,8 @@ public boolean isAutoRegisterHooks() { @NonNull public String getCheckoutCredentialsId() { for (SCMTrait t : traits) { - if (t instanceof SSHCheckoutTrait) { - return StringUtils.defaultString(((SSHCheckoutTrait) t).getCredentialsId(), BitbucketSCMSource + if (t instanceof SSHCheckoutTrait sshTrait) { + return StringUtils.defaultString(sshTrait.getCredentialsId(), BitbucketSCMSource .DescriptorImpl.ANONYMOUS); } } @@ -361,7 +362,7 @@ public String getCheckoutCredentialsId() { @RestrictedSince("2.2.0") @DataBoundSetter public void setCheckoutCredentialsId(String checkoutCredentialsId) { - traits.removeIf(trait -> trait instanceof SSHCheckoutTrait); + traits.removeIf(SSHCheckoutTrait.class::isInstance); if (checkoutCredentialsId != null && !BitbucketSCMSource.DescriptorImpl.SAME.equals(checkoutCredentialsId)) { traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); } @@ -372,8 +373,8 @@ public void setCheckoutCredentialsId(String checkoutCredentialsId) { @RestrictedSince("2.2.0") public String getPattern() { for (SCMTrait trait : traits) { - if (trait instanceof RegexSCMSourceFilterTrait) { - return ((RegexSCMSourceFilterTrait) trait).getRegex(); + if (trait instanceof RegexSCMSourceFilterTrait regexTrait) { + return regexTrait.getRegex(); } } return ".*"; @@ -421,8 +422,8 @@ public String getEndpointJenkinsRootUrl() { @NonNull public String getIncludes() { for (SCMTrait trait : traits) { - if (trait instanceof WildcardSCMHeadFilterTrait) { - return ((WildcardSCMHeadFilterTrait) trait).getIncludes(); + if (trait instanceof WildcardSCMHeadFilterTrait wildcardTrait) { + return wildcardTrait.getIncludes(); } } return "*"; @@ -435,12 +436,11 @@ public String getIncludes() { public void setIncludes(@NonNull String includes) { for (int i = 0; i < traits.size(); i++) { SCMTrait trait = traits.get(i); - if (trait instanceof WildcardSCMHeadFilterTrait) { - WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; - if ("*".equals(includes) && "".equals(existing.getExcludes())) { + if (trait instanceof WildcardSCMHeadFilterTrait wildcardTrait) { + if ("*".equals(includes) && "".equals(wildcardTrait.getExcludes())) { traits.remove(i); } else { - traits.set(i, new WildcardSCMHeadFilterTrait(includes, existing.getExcludes())); + traits.set(i, new WildcardSCMHeadFilterTrait(includes, wildcardTrait.getExcludes())); } return; } @@ -456,8 +456,8 @@ public void setIncludes(@NonNull String includes) { @NonNull public String getExcludes() { for (SCMTrait trait : traits) { - if (trait instanceof WildcardSCMHeadFilterTrait) { - return ((WildcardSCMHeadFilterTrait) trait).getExcludes(); + if (trait instanceof WildcardSCMHeadFilterTrait wildcardTrait) { + return wildcardTrait.getExcludes(); } } return ""; @@ -470,12 +470,11 @@ public String getExcludes() { public void setExcludes(@NonNull String excludes) { for (int i = 0; i < traits.size(); i++) { SCMTrait trait = traits.get(i); - if (trait instanceof WildcardSCMHeadFilterTrait) { - WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; - if ("*".equals(existing.getIncludes()) && "".equals(excludes)) { + if (trait instanceof WildcardSCMHeadFilterTrait wildcardTrait) { + if ("*".equals(wildcardTrait.getIncludes()) && "".equals(excludes)) { traits.remove(i); } else { - traits.set(i, new WildcardSCMHeadFilterTrait(existing.getIncludes(), excludes)); + traits.set(i, new WildcardSCMHeadFilterTrait(wildcardTrait.getIncludes(), excludes)); } return; } @@ -569,37 +568,33 @@ public List retrieveActions(@NonNull SCMNavigatorOwner owner, BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials); - BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null); - BitbucketTeam team = bitbucket.getTeam(); - if (team != null) { - String defaultTeamUrl; - if (team instanceof BitbucketServerProject) { - defaultTeamUrl = serverUrl + "/projects/" + team.getName(); + try (BitbucketApi client = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, projectKey, null)) { + BitbucketTeam team = client.getTeam(); + String avatarURL = null; + String teamURL; + String teamDisplayName; + if (team != null) { + avatarURL = team.getAvatar(); + teamURL = team.getLink("html"); + teamDisplayName = StringUtils.defaultIfBlank(team.getDisplayName(), team.getName()); + if (StringUtils.isNotBlank(teamURL)) { + if (team instanceof BitbucketCloudWorkspace wks) { + teamURL = serverUrl + "/" + wks.getSlug(); + } else { + teamURL = serverUrl + "/projects/" + team.getName(); + } + } + listener.getLogger().printf("Team: %s%n", HyperlinkNote.encodeTo(teamURL, teamDisplayName)); } else { - defaultTeamUrl = serverUrl + "/" + team.getName(); + teamURL = serverUrl + "/" + repoOwner; + teamDisplayName = repoOwner; + listener.getLogger().println("Could not resolve team details"); } - String teamUrl = StringUtils.defaultIfBlank(team.getLink("html"), defaultTeamUrl); - String teamDisplayName = StringUtils.defaultIfBlank(team.getDisplayName(), team.getName()); - result.add(new ObjectMetadataAction( - teamDisplayName, - null, - teamUrl - )); - result.add(new BitbucketTeamMetadataAction(serverUrl, credentials, team.getName())); - result.add(new BitbucketLink("icon-bitbucket-logo", teamUrl)); - listener.getLogger().printf("Team: %s%n", HyperlinkNote.encodeTo(teamUrl, teamDisplayName)); - } else { - String teamUrl = serverUrl + "/" + repoOwner; - result.add(new ObjectMetadataAction( - repoOwner, - null, - teamUrl - )); - result.add(new BitbucketTeamMetadataAction(null, null, null)); - result.add(new BitbucketLink("icon-bitbucket-logo", teamUrl)); - listener.getLogger().println("Could not resolve team details"); + result.add(new ObjectMetadataAction(teamDisplayName, null, teamURL)); + result.add(new BitbucketTeamAvatarMetadataAction(avatarURL, serverUrl, owner.getFullName(), credentialsId)); + result.add(new BitbucketLink("icon-bitbucket-logo", teamURL)); + return result; } - return result; } @Symbol("bitbucket") @@ -629,7 +624,6 @@ public String getIconClassName() { return "icon-bitbucket-scm-navigator"; } - @SuppressWarnings("unchecked") @Override public SCMNavigator newInstance(String name) { BitbucketSCMNavigator instance = new BitbucketSCMNavigator(StringUtils.defaultString(name)); diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java index 144ce6f5e..bae9d3619 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMSource.java @@ -43,6 +43,7 @@ import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; import com.cloudbees.jenkins.plugins.bitbucket.hooks.HasPullRequests; +import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketRepoAvatarMetadataAction; import com.cloudbees.jenkins.plugins.bitbucket.impl.extension.BitbucketEnvVarExtension; import com.cloudbees.jenkins.plugins.bitbucket.impl.extension.GitClientAuthenticatorExtension; import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketApiUtils; @@ -65,12 +66,9 @@ import hudson.RestrictedSince; import hudson.Util; import hudson.console.HyperlinkNote; -import hudson.init.InitMilestone; -import hudson.init.Initializer; import hudson.model.Action; import hudson.model.Actionable; import hudson.model.Item; -import hudson.model.Items; import hudson.model.TaskListener; import hudson.plugins.git.GitSCM; import hudson.scm.SCM; @@ -97,7 +95,6 @@ import java.util.logging.Logger; import jenkins.authentication.tokens.api.AuthenticationTokens; import jenkins.model.Jenkins; -import jenkins.plugins.git.MergeWithGitSCMExtension; import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadCategory; @@ -152,14 +149,6 @@ public class BitbucketSCMSource extends SCMSource { private static final String CLOUD_REPO_TEMPLATE = "{/owner,repo}"; private static final String SERVER_REPO_TEMPLATE = "/projects{/owner}/repos{/repo}"; - /** - * Mapping classes after refactoring for backward compatibility. - */ - @Initializer(before = InitMilestone.PLUGINS_STARTED) - public static void aliases() { - Items.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.plugins.bitbucket.MergeWithGitSCMExtension", MergeWithGitSCMExtension.class); - } - /** How long to delay events received from Bitbucket in order to allow the API caches to sync. */ private static /*mostly final*/ int eventDelaySeconds = Math.min( @@ -1107,26 +1096,27 @@ protected List retrieveActions(@CheckForNull SCMSourceEvent event, throws IOException, InterruptedException { // TODO when we have support for trusted events, use the details from event if event was from trusted source List result = new ArrayList<>(); - final BitbucketApi bitbucket = buildBitbucketClient(); - gatherPrimaryCloneLinks(bitbucket); - BitbucketRepository r = bitbucket.getRepository(); - result.add(new BitbucketRepoMetadataAction(r)); - String defaultBranch = bitbucket.getDefaultBranch(); - if (StringUtils.isNotBlank(defaultBranch)) { - result.add(new BitbucketDefaultBranch(repoOwner, repository, defaultBranch)); + try (BitbucketApi bitbucket = buildBitbucketClient()) { + gatherPrimaryCloneLinks(bitbucket); + BitbucketRepository r = bitbucket.getRepository(); + result.add(new BitbucketRepoAvatarMetadataAction(r)); + String defaultBranch = bitbucket.getDefaultBranch(); + if (StringUtils.isNotBlank(defaultBranch)) { + result.add(new BitbucketDefaultBranch(repoOwner, repository, defaultBranch)); + } + UriTemplate template; + if (BitbucketApiUtils.isCloud(getServerUrl())) { + template = UriTemplate.fromTemplate(getServerUrl() + CLOUD_REPO_TEMPLATE); + } else { + template = UriTemplate.fromTemplate(getServerUrl() + SERVER_REPO_TEMPLATE); + } + String url = template + .set("owner", repoOwner) + .set("repo", repository) + .expand(); + result.add(new BitbucketLink("icon-bitbucket-repo", url)); + result.add(new ObjectMetadataAction(r.getRepositoryName(), null, url)); } - UriTemplate template; - if (BitbucketApiUtils.isCloud(getServerUrl())) { - template = UriTemplate.fromTemplate(getServerUrl() + CLOUD_REPO_TEMPLATE); - } else { - template = UriTemplate.fromTemplate(getServerUrl() + SERVER_REPO_TEMPLATE); - } - String url = template - .set("owner", repoOwner) - .set("repo", repository) - .expand(); - result.add(new BitbucketLink("icon-bitbucket-repo", url)); - result.add(new ObjectMetadataAction(r.getRepositoryName(), null, url)); return result; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketTeamMetadataAction.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketTeamMetadataAction.java deleted file mode 100644 index 126b8a863..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketTeamMetadataAction.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, CloudBees, Inc. - * - * 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 com.cloudbees.jenkins.plugins.bitbucket; - -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; -import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException; -import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCache; -import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCacheSource; -import com.cloudbees.plugins.credentials.CredentialsMatchers; -import com.cloudbees.plugins.credentials.CredentialsProvider; -import com.cloudbees.plugins.credentials.common.StandardCredentials; -import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; -import hudson.model.Item; -import hudson.security.ACL; -import hudson.security.ACLContext; -import java.io.IOException; -import java.io.Serializable; -import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; -import jenkins.authentication.tokens.api.AuthenticationTokens; -import jenkins.scm.api.metadata.AvatarMetadataAction; - -/** - * Invisible property that retains information about Bitbucket team. - */ -public class BitbucketTeamMetadataAction extends AvatarMetadataAction { - /** - * - */ - private static final long serialVersionUID = 1L; - - /** - * Our logger. - */ - private static final Logger LOGGER = Logger.getLogger(BitbucketTeamMetadataAction.class.getName()); - - private final BitbucketAvatarCacheSource avatarSource; - - public BitbucketTeamMetadataAction(String serverUrl, StandardCredentials credentials, String team) { - avatarSource = new BitbucketAvatarCacheSource(serverUrl, credentials, team); - } - - public static class BitbucketAvatarCacheSource implements AvatarCacheSource, Serializable { - private static final long serialVersionUID = 1L; - private final String serverUrl; - private StandardCredentials credentials; - private final String repoOwner; - - public BitbucketAvatarCacheSource(String serverUrl, StandardCredentials credentials, String repoOwner) { - this.serverUrl = serverUrl; - this.credentials = credentials; - this.repoOwner = repoOwner; - LOGGER.log(Level.INFO, "Created: {0}", this.toString()); - } - - @Override - public AvatarImage fetch(StandardCredentials credentials) { - try { - return doFetch(credentials); - } catch (IOException e) { - if (e.getCause() instanceof BitbucketRequestException) { - BitbucketRequestException bre = (BitbucketRequestException) e.getCause(); - if (bre.getHttpCode()==401) { - // credentials not updated here maybe we need to refresh them - StandardCredentials standardCredentials = findCredentials(); - // try again with refreshed credentials - // TODO compare previous and new token if we really need to try again - if (standardCredentials != null) { - this.credentials = standardCredentials; - try { - return doFetch(this.credentials); - } catch (IOException | InterruptedException ex) { - LOGGER.log(Level.INFO, ex.getClass().getName()+": " + e.getMessage(), e); - } - } - } - } - LOGGER.log(Level.INFO, "IOException: " + e.getMessage(), e); - } catch (InterruptedException e) { - LOGGER.log(Level.INFO, "InterruptedException: " + e.getMessage(), e); - } - return null; - } - - private StandardCredentials findCredentials() { - try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) { - return CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentialsInItem( - credentials.getClass(), - (Item) null, // context - ACL.SYSTEM2, - URIRequirementBuilder.fromUri(serverUrl).build() - ), - CredentialsMatchers.allOf( - CredentialsMatchers.withId(credentials.getId()), - CredentialsMatchers.anyOf(CredentialsMatchers.instanceOf(credentials.getClass())) - ) - ); - } - } - - private AvatarImage doFetch(StandardCredentials credentials) throws IOException, InterruptedException { - BitbucketAuthenticator authenticator = AuthenticationTokens - .convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials); - BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, repoOwner, null, null); - return bitbucket.getTeamAvatar(); - } - - @Override - public AvatarImage fetch() { - if(this.credentials==null) { - throw new UnsupportedOperationException("this method can be used only with credentials"); - } - return this.fetch(this.credentials); - } - - @Override - public String hashKey() { - return "" + serverUrl + "::" + repoOwner + "::" + (credentials != null ? credentials.getId() : ""); - } - - @Override - public boolean canFetch() { - return (serverUrl != null && repoOwner != null && !serverUrl.trim().isEmpty() - && !repoOwner.trim().isEmpty()); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hashCode(hashKey()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - BitbucketAvatarCacheSource that = (BitbucketAvatarCacheSource) o; - return this.hashKey().equals(that.hashKey()); - } - - @Override - public String toString() { - return "BitbucketAvatarSource(" + hashKey() + ")"; - } - } - /** - * {@inheritDoc} - */ - @Override - public String getAvatarImageOf(String size) { - // fall back to the generic bitbucket org icon if no avatar provided - return avatarSource == null - ? avatarIconClassNameImageOf(getAvatarIconClassName(), size) - : AvatarCache.buildUrl(avatarSource, size); - } - - - /** - * {@inheritDoc} - */ - @Override - public String getAvatarIconClassName() { - return avatarSource == null ? "icon-bitbucket-logo" : null; - } - - /** - * {@inheritDoc} - */ - @Override - public String getAvatarDescription() { - return Messages.BitbucketTeamMetadataAction_IconDescription(); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - BitbucketTeamMetadataAction that = (BitbucketTeamMetadataAction) o; - if (this.avatarSource == null) { - return that.avatarSource == null; - } - return this.avatarSource.equals(that.avatarSource); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hashCode(avatarSource); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "BitbucketTeamMetadataAction{" + - ", avatarSource='" + avatarSource + '\'' + - '}'; - } -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/AbstractBitbucketTeam.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/AbstractBitbucketTeam.java deleted file mode 100644 index b6d7b4f9d..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/AbstractBitbucketTeam.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016-2017, CloudBees, Inc. - * - * 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 com.cloudbees.jenkins.plugins.bitbucket.api; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import java.util.List; -import java.util.Map; - -/** - * Shared Code between two team implementations - */ -public abstract class AbstractBitbucketTeam implements BitbucketTeam { - - @JsonProperty("username") - private String name; - - @JsonProperty("display_name") - private String displayName; - - @JsonProperty("links") - @JsonDeserialize(keyAs = String.class, contentUsing = BitbucketHref.Deserializer.class) - private Map> links; - - @Override - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - @JsonIgnore - public Map> getLinks() { - return links; - } - - @JsonIgnore - public void setLinks(Map> links) { - this.links = links; - } - - @Override - @JsonIgnore - public String getLink(String name) { - if (links == null) { - return null; - } - List hrefs = links.get(name); - if (hrefs == null || hrefs.isEmpty()) { - return null; - } - BitbucketHref href = hrefs.get(0); - return href == null ? null : href.getHref(); - } -} 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 a1efc1d8a..fb5a6ab64 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 @@ -23,7 +23,6 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api; -import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCacheSource.AvatarImage; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.filesystem.BitbucketSCMFile; import edu.umd.cs.findbugs.annotations.CheckForNull; @@ -32,6 +31,7 @@ import java.io.InputStream; import java.util.List; import jenkins.scm.api.SCMFile; +import jenkins.scm.impl.avatars.AvatarImage; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -247,10 +247,22 @@ boolean checkPathExists(@NonNull String branchOrHash, @NonNull String path) * * @return the team profile of the current owner, or {@code null} if {@link #getOwner()} is not a team ID. * @throws IOException if there was a network communications error. - * @throws InterruptedException if interrupted while waiting on remote communications. + * @deprecated Use {@link #getAvatar(String)} with the avatar url link gather from repository, project, workspace or user. + */ + @CheckForNull + @Deprecated + AvatarImage getTeamAvatar() throws IOException; + + /** + * Returns an Avatar image from the given URL. + *

+ * The URL link could come from repository, project, workspace or user links. + * + * @return the resource image. + * @throws IOException if there was a network communications error. */ @CheckForNull - AvatarImage getTeamAvatar() throws IOException, InterruptedException; + AvatarImage getAvatar(@NonNull String url) throws IOException; /** * Returns the repositories where the user has the given role. @@ -328,4 +340,10 @@ List getRepositories(@CheckForNull UserRoleInRepo @NonNull @Restricted(NoExternalUse.class) SCMFile getFile(@NonNull BitbucketSCMFile file) throws IOException, InterruptedException; + + /** + * {@inheritDoc} + */ + @Override + void close() throws IOException; } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketCloudWorkspace.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketCloudWorkspace.java index a9be35c1f..2454fa4c5 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketCloudWorkspace.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketCloudWorkspace.java @@ -55,6 +55,10 @@ public String getName() { return name; } + public void setName(String name) { + this.name = name; + } + @Override public String getDisplayName() { return name; @@ -76,15 +80,8 @@ public Map> getLinks() { } @Override - public String getLink(String name) { - if (links == null) { - return null; - } - List hrefs = links.get(name); - if (hrefs == null || hrefs.isEmpty()) { - return null; - } - BitbucketHref href = hrefs.get(0); - return href == null ? null : href.getHref(); + public String getAvatar() { + return getLink("avatar"); } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketProject.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketProject.java index 00d4c3275..3be5de845 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketProject.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketProject.java @@ -23,19 +23,31 @@ */ package com.cloudbees.jenkins.plugins.bitbucket.api; -public class BitbucketProject { +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; +import java.util.Map; + +public class BitbucketProject implements BitbucketTeam { private String key; private String name; + @JsonDeserialize(keyAs = String.class, contentUsing = BitbucketHref.Deserializer.class) + private Map> links; public String getKey() { return key; } + @Override public String getName() { return name; } + @Override + public String getDisplayName() { + return name; + } + public void setKey(String key) { this.key = key; } @@ -44,8 +56,36 @@ public void setName(String name) { this.name = name; } + @Override + public Map> getLinks() { + return links; + } + + public void setLinks(Map> links) { + this.links = links; + } + + @Override + public String getAvatar() { + return getLink("avatar"); + } + + @Override + public String getLink(String name) { + if (links == null) { + return null; + } + List hrefs = links.get(name); + if (hrefs == null || hrefs.isEmpty()) { + return null; + } + BitbucketHref href = hrefs.get(0); + return href == null ? null : href.getHref(); + } + @Override public String toString() { return "{ key=" + key + ", name=" + name + "}"; } + } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketRepository.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketRepository.java index 2b0887642..b03260e37 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketRepository.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketRepository.java @@ -76,10 +76,35 @@ public interface BitbucketRepository { */ boolean isArchived(); + /** + * Get Link based on name + * + * @param name - link type - one of(self, html, avatar) + * @return href string if there is one, else null + */ + default String getLink(String name) { + Map> links = getLinks(); + if (links == null) { + return null; + } + List hrefs = links.get(name); + if (hrefs == null || hrefs.isEmpty()) { + return null; + } + BitbucketHref href = hrefs.get(0); + return href == null ? null : href.getHref(); + } + /** * Gets the links for this repository. * @return the links for this repository. */ Map> getLinks(); + /** + * Return the avatar associated to the team or project name. + * + * @return the URL of the avatar + */ + String getAvatar(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketTeam.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketTeam.java index 0fa0789f9..e7b32d8c3 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketTeam.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/api/BitbucketTeam.java @@ -46,7 +46,7 @@ public interface BitbucketTeam { * * @return the links of the project. */ - Map> getLinks(); + Map> getLinks(); /** * Get Link based on name @@ -54,5 +54,23 @@ public interface BitbucketTeam { * @param name - link type - one of(self, html, avatar) * @return href string if there is one, else null */ - String getLink(String name); + default String getLink(String name) { + Map> links = getLinks(); + if (links == null) { + return null; + } + List hrefs = links.get(name); + if (hrefs == null || hrefs.isEmpty()) { + return null; + } + BitbucketHref href = hrefs.get(0); + return href == null ? null : href.getHref(); + } + + /** + * Return the avatar associated to the team or project name. + * + * @return the URL of the avatar + */ + String getAvatar(); } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCache.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCache.java deleted file mode 100644 index ca82ac40a..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCache.java +++ /dev/null @@ -1,632 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2017, CloudBees, Inc. - * - * 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 com.cloudbees.jenkins.plugins.bitbucket.avatars; - -import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCacheSource.AvatarImage; -import edu.umd.cs.findbugs.annotations.CheckForNull; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import hudson.Extension; -import hudson.ExtensionList; -import hudson.Util; -import hudson.model.RootAction; -import hudson.model.UnprotectedRootAction; -import hudson.util.DaemonThreadFactory; -import hudson.util.HttpResponses; -import hudson.util.NamingThreadFactory; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletResponse; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import javax.imageio.ImageIO; -import jenkins.model.Jenkins; -import org.apache.commons.lang.StringUtils; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.StaplerRequest2; -import org.kohsuke.stapler.StaplerResponse2; - -import static java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION; -import static java.awt.RenderingHints.KEY_INTERPOLATION; -import static java.awt.RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY; -import static java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC; - -/** - * An avatar cache that will serve URLs that have been recently registered - * through {@link #buildUrl(String, String)}. - * - * @since 2.2.0 - */ -@Extension -public class AvatarCache implements UnprotectedRootAction { - - /** - * URI For this action - */ - private static final String ActionURI = "custom-avatar-cache"; - - /** - * Maximum concurrent requests to fetch images. - */ - private static final int CONCURRENT_REQUEST_LIMIT = 4; - - /** - * The cache of entries. Unused entries will be removed over time. - */ - private final ConcurrentMap cache = new ConcurrentHashMap<>(); - - /** - * A background thread pool to refresh images. - */ - private final ThreadPoolExecutor service = new ThreadPoolExecutor(CONCURRENT_REQUEST_LIMIT, - CONCURRENT_REQUEST_LIMIT, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), - new NamingThreadFactory(new DaemonThreadFactory(), getClass().getName())); - - /** - * The lock to ensure we prevent concurrent requests for the same URL. - */ - private final Object serviceLock = new Object(); - - /** - * The iterator that searches for unused entries. The search is amortized over - * every access. - */ - private Iterator> iterator = null; - - /** - * The time this service was started (used as the last modified for generated - * avatars). - */ - private final long startedTime; - - /** - * Constructor. - */ - public AvatarCache() { - service.allowCoreThreadTimeOut(true); - // Remove any milliseconds from the started time to the nearest second - startedTime = System.currentTimeMillis() / 1000L * 1000L; - } - - /** - * Builds the URL for the cached avatar image of the required size. - * - * @param url the URL of the source avatar image. - * @param size the size of the image. - * @return the URL of the cached image. - * @throws IllegalStateException if called outside of a request handling thread. - */ - public static String buildUrl(@NonNull String url, @NonNull String size) { - return buildUrl(new UrlAvatarCacheSource(url), size); - } - - /** - * Builds the URL for the cached avatar image of the required size. - * - * @param source source avatar image definition. - * @param size the size of the image. - * @return the URL of the cached image. - * @throws IllegalStateException if called outside of a request handling thread. - */ - public static String buildUrl(@NonNull AvatarCacheSource source, @NonNull String size) { - Jenkins j = Jenkins.get(); - AvatarCache instance = ExtensionList.lookup(RootAction.class).get(AvatarCache.class); - if (instance == null) { - throw new AssertionError(); - } - String key = Util.getDigestOf(AvatarCache.class.getName() + source.hashKey()); - // seed the cache - instance.getCacheEntry(key, source); - try { - return j.getRootUrlFromRequest() + instance.getUrlName() + "/" + Util.rawEncode(key) + ".png?size=" - + URLEncoder.encode(size, StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError("JLS specification mandates support for UTF-8 encoding", e); - } - } - - /** - * Scales the provided image up or down to reach the target size while - * preserving aspect ratio. - * - * @param src the image to scale - * @param size the size to scale to. - * @return an image of {@code size x size}. - */ - @NonNull - private static BufferedImage scaleImage(@NonNull BufferedImage src, int size) { - BufferedImage imageSrc = src; - int newWidth; - int newHeight; - if (src.getWidth() > src.getHeight()) { - newWidth = size; - newHeight = size * src.getHeight() / src.getWidth(); - } else if (src.getHeight() > src.getWidth()) { - newWidth = size * src.getWidth() / src.getHeight(); - newHeight = size; - } else { - newWidth = newHeight = size; - } - boolean flushSrc = false; - if (newWidth <= src.getWidth() * 6 / 7 && newHeight <= src.getWidth() * 6 / 7) { - // when scaling down, you get better image quality if you scale down in multiple - // rounds - // see https://community.oracle.com/docs/DOC-983611 - // we scale each round by 6/7 = ~85% as this gives nicer looking images - int curWidth = src.getWidth(); - int curHeight = src.getHeight(); - // we want to break the rounds and do the final round and centre when the src - // image is this size - final int penultimateSize = size * 7 / 6; - while (true) { - curWidth = curWidth - curWidth / 7; - curHeight = curHeight - curHeight / 7; - if (curWidth <= penultimateSize && curHeight <= penultimateSize) { - // we are within one round of target size let's go - break; - } - BufferedImage tmp = new BufferedImage(curWidth, curHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = tmp.createGraphics(); - try { - // important, if we don't set these two hints then scaling will not work - // headless - g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC); - g.setRenderingHint(KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY); - g.scale(((double) curWidth) / src.getWidth(), ((double) curHeight) / src.getHeight()); - g.drawImage(src, 0, 0, null); - } finally { - g.dispose(); - } - if (flushSrc) { - imageSrc.flush(); - } - imageSrc = tmp; - flushSrc = true; - } - } - BufferedImage tmp = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = tmp.createGraphics(); - try { - // important, if we don't set these two hints then scaling will not work - // headless - g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC); - g.setRenderingHint(KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY); - g.scale(((double) newWidth) / imageSrc.getWidth(), ((double) newHeight) / imageSrc.getHeight()); - g.drawImage(imageSrc, (size - newWidth) / 2, (size - newHeight) / 2, null); - } finally { - g.dispose(); - } - if (flushSrc) { - imageSrc.flush(); - } - imageSrc = tmp; - return imageSrc; - } - - /** - * Generates a consistent (for any given seed) 5x5 symmetric pixel avatar that - * should be unique but recognizable. - * - * @param seed the seed. - * @param size the size. - * @return the image. - */ - private static BufferedImage generateAvatar(@NonNull String seed, int size) { - byte[] bytes; - try { - // we want a consistent image across reboots, so just take a hash of the seed - // if the seed changes we get a new hash and a new image! - MessageDigest d = MessageDigest.getInstance("MD5"); - bytes = d.digest(seed.getBytes(StandardCharsets.UTF_8)); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("JLS specification mandates support for MD5 message digest", e); - } - BufferedImage canvas = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = canvas.createGraphics(); - try { - // we want the colour in the range 16-245 to prevent pure white and pure black - // 0xdf == 1101111 so we throw away the 32 place and add in 16 to give 16 on - // either side - g.setColor(new Color(bytes[0] & 0xdf + 16, bytes[1] & 0xdf + 16, bytes[2] & 0xdf + 16)); - int pSize = size / 5; - // likely there will be some remainder from dividing by 5, so half the remainder - // will be used - // as an offset to centre the image - int pOffset = (size - pSize * 5) / 2; - for (int y = 0; y < 5; y++) { - for (int x = 0; x < 5; x++) { - byte bit = (byte) (1 << Math.min(x, 4 - x)); - if ((bytes[3 + y] & bit) != 0) { - g.fillRect(pOffset + x * pSize, pOffset + y * pSize, pSize, pSize); - } - } - } - } finally { - g.dispose(); - } - return canvas; - } - - /** - * {@inheritDoc} - */ - @Override - public String getIconFileName() { - return null; - } - - /** - * {@inheritDoc} - */ - @Override - public String getDisplayName() { - return null; - } - - /** - * {@inheritDoc} - */ - @Override - public String getUrlName() { - return ActionURI; - } - - /** - * Serves the cached image. - * - * @param req the request. - * @param requestedSize the requested size (defaults to {@code 48x48} if unspecified). - * @return the response. - */ - public HttpResponse doDynamic(StaplerRequest2 req, @QueryParameter String requestedSize) { - if (StringUtils.isBlank(req.getRestOfPath())) { - return HttpResponses.notFound(); - } - String key = req.getRestOfPath().substring(1); - if (!key.endsWith(".png")) { - return HttpResponses.notFound(); - } - key = StringUtils.removeEnd(key, ".png"); - String size = StringUtils.defaultIfBlank(requestedSize, "48x48"); - int targetSize = 48; - int index = size.toLowerCase(Locale.ENGLISH).indexOf('x'); - // we will only resize images in the 16x16 - 128x128 range - if (index < 2) { - try { - targetSize = Math.min(128, Math.max(16, Integer.parseInt(StringUtils.trim(size)))); - } catch (NumberFormatException e) { - // ignore - } - } else { - try { - targetSize = Math.min(128, Math.max(16, Integer.parseInt(StringUtils.trim(size.substring(0, index))))); - } catch (NumberFormatException e) { - // ignore - } - } - final CacheEntry avatar = getCacheEntry(key, null); - final long since = req.getDateHeader("If-Modified-Since"); - - // If no avatar, all is unmodified - if (avatar == null || !avatar.canFetch()) { - if (startedTime <= since) { - return new HttpResponse() { - @Override - public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) - throws IOException, ServletException { - rsp.addDateHeader("Last-Modified", startedTime); - rsp.addHeader("Cache-control", "max-age=365000000, immutable, public"); - rsp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - }; - } - // we will generate avatars if the URL is not HTTP based - // since the url string will not magically turn itself into a HTTP url this - // avatar is immutable - return new ImageResponse(generateAvatar(avatar == null ? "" : avatar.source.hashKey(), targetSize), true, - startedTime, "max-age=365000000, immutable, public"); - } - - if (avatar.pending() && avatar.image == null) { - // serve a temporary avatar until we get the remote one, no caching as we could - // have the real deal - // real soon now - return new ImageResponse(generateAvatar(avatar.source.hashKey(), targetSize), true, -1L, - "no-cache, public"); - } - if (avatar.lastModified <= since) { - return new HttpResponse() { - - @Override - public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) - throws IOException, ServletException { - rsp.addDateHeader("Last-Modified", avatar.lastModified); - rsp.addHeader("Cache-control", "max-age=3600, public"); - rsp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } - }; - } - // If no image, generate a temp avatar - if (avatar.image == null) { - // we can retry in an hour - return new ImageResponse(generateAvatar(avatar.source.hashKey(), targetSize), true, -1L, - "max-age=3600, public"); - } - - BufferedImage image = avatar.image; - boolean flushImage = false; - if (image.getWidth() != targetSize || image.getHeight() != targetSize) { - image = scaleImage(image, targetSize); - flushImage = true; - } - return new ImageResponse(image, flushImage, avatar.lastModified, "max-age=3600, public"); - } - - /** - * Retrieves the entry from the cache. - * - * @param key the cache key. - * @param url the URL to fetch if the entry is missing or {@code null} to - * perform a read-only check. - * @return the entry or {@code null} if a read-only check found no matching - * entry. - */ - @Nullable - private CacheEntry getCacheEntry(@NonNull final String key, @Nullable final AvatarCacheSource source) { - CacheEntry entry = cache.get(key); - if (entry == null) { - synchronized (serviceLock) { - entry = cache.get(key); - if (entry == null) { - if (source == null) { - return null; - } - entry = new CacheEntry(source, service.submit(new FetchImage(source))); - cache.put(key, entry); - } - } - } else { - if (entry.isStale()) { - synchronized (serviceLock) { - if (!entry.pending()) { - entry.setFuture(service.submit(new FetchImage(entry.source))); - } - } - } - } - entry.touch(); - if (iterator == null || !iterator.hasNext()) { - synchronized (serviceLock) { - if (iterator == null || !iterator.hasNext()) { - iterator = cache.entrySet().iterator(); - } - } - } else { - synchronized (iterator) { - // process one entry in the cache each access - if (iterator.hasNext()) { - Map.Entry next = iterator.next(); - if (next.getValue().isUnused()) { - iterator.remove(); - } - } else { - iterator = null; - } - } - } - return entry; - } - - /** - * A cache entry. - */ - private static class CacheEntry { - - /** - * Source for avatar - */ - private final AvatarCacheSource source; - - /** - * The cached image or {@code null} if not retrieved yet. - */ - @CheckForNull - private BufferedImage image; - - /** - * The last modified timestamp, comparable to - * {@link System#currentTimeMillis()}. - */ - private long lastModified; - - /** - * The last accessed timestamp, comparable to - * {@link System#currentTimeMillis()}, {@code -1L} signals never accessed. - */ - private long lastAccessed = -1L; - - /** - * The queued request to retrieve the image from the {@link #url}. - */ - private Future future; - - private CacheEntry(AvatarCacheSource source, BufferedImage image, long lastModified) { - this.source = source; - if (image.getHeight() > 128 || image.getWidth() > 128) { - // limit the amount of storage - this.image = scaleImage(image, 128); - image.flush(); - } else { - this.image = image; - } - this.lastModified = lastModified < 0 ? System.currentTimeMillis() : lastModified; - } - - /** - * Check if this entry is fetch-able - * - * @return whether the entry can be fetched - */ - public boolean canFetch() { - return (source != null && source.canFetch()); - } - - private CacheEntry(AvatarCacheSource source, Future future) { - this.source = source; - this.image = null; - this.lastModified = System.currentTimeMillis(); - this.future = future; - } - - private CacheEntry(AvatarCacheSource source) { - this.source = source; - this.lastModified = System.currentTimeMillis(); - } - - private synchronized boolean pending() { - if (future == null) { - return false; - } - if (future.isDone()) { - try { - CacheEntry pending = future.get(); - if (pending.image != null && image != null) { - image.flush(); - } - if (pending.image != null) { - image = pending.image; - } - lastModified = pending.lastModified; - future = null; - return false; - } catch (InterruptedException | ExecutionException e) { - // ignore - } - - } - return true; - } - - private synchronized void setFuture(Future future) { - this.future = future; - } - - private synchronized boolean isStale() { - return System.currentTimeMillis() - lastModified > TimeUnit.MINUTES.toMillis(Long.getLong(AvatarCache.class.getName()+".stale.ttl",60)); - } - - private void touch() { - lastAccessed = System.currentTimeMillis(); - } - - private boolean isUnused() { - return lastAccessed > 0L && System.currentTimeMillis() - lastAccessed > TimeUnit.MINUTES.toMillis(Long.getLong(AvatarCache.class.getName()+".unused.ttl",60)); - } - } - - /** - * A {@link HttpResponse} that serves a {@link BufferedImage} as a PNG - */ - private static class ImageResponse implements HttpResponse { - private final BufferedImage image; - private final boolean flushImage; - private final String cacheControl; - - private final long lastModified; - - private ImageResponse(BufferedImage image, boolean flushImage, long lastModified, String cacheControl) { - this.cacheControl = cacheControl; - this.image = image; - this.flushImage = flushImage; - this.lastModified = lastModified; - } - - /** - * {@inheritDoc} - */ - @Override - public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object node) - throws IOException, ServletException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - ImageIO.write(image, "png", bos); - } finally { - if (flushImage) { - image.flush(); - } - } - final byte[] bytes = bos.toByteArray(); - if (lastModified > 0) { - rsp.addDateHeader("Last-Modified", lastModified); - } - rsp.addHeader("Cache-control", cacheControl); - rsp.setContentType("image/png"); - rsp.setContentLength(bytes.length); - rsp.getOutputStream().write(bytes); - } - } - - /** - * A task to fetch an image from a remote URL. - */ - private static class FetchImage implements Callable { - private final AvatarCacheSource source; - - private FetchImage(@NonNull AvatarCacheSource source) { - this.source = source; - } - - /** - * {@inheritDoc} - */ - @Override - public CacheEntry call() throws Exception { - AvatarImage image = source.fetch(); - // If no image, return no image - if (image == null) { - return new CacheEntry(source); - } - return new CacheEntry(source, image.image, image.lastModified); - } - } -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCacheSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCacheSource.java deleted file mode 100644 index 2d89fa92d..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCacheSource.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, CloudBees, Inc. - * - * 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 com.cloudbees.jenkins.plugins.bitbucket.avatars; - -import com.cloudbees.plugins.credentials.common.StandardCredentials; -import java.awt.image.BufferedImage; - -/** - * - * Interface for Avatar Cache Item Source - * - */ -public interface AvatarCacheSource { - - /** - * Holds Image and lastModified date - */ - public static class AvatarImage { - public final BufferedImage image; - public final long lastModified; - - public static final AvatarImage EMPTY = new AvatarImage(null, 0); - - public AvatarImage(final BufferedImage image, final long lastModified) { - this.image = image; - this.lastModified = lastModified; - } - } - - /** - * - * @deprecated use {@link #fetch(StandardCredentials)} - * Fetch image from source - * - * @return AvatarImage object - */ - @Deprecated - AvatarImage fetch(); - - /** - * - * Fetch image from source - * @param credentials the credentials to use - * @return AvatarImage object - */ - AvatarImage fetch(StandardCredentials credentials); - - /** - * Get unique hashKey for this item - * - * @return AvatarImage object - */ - String hashKey(); - - /** - * Make sure we can fetch - * - * @return true if can fetch - */ - boolean canFetch(); -} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/UrlAvatarCacheSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/UrlAvatarCacheSource.java deleted file mode 100644 index 70306eed4..000000000 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/UrlAvatarCacheSource.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2016, CloudBees, Inc. - * - * 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 com.cloudbees.jenkins.plugins.bitbucket.avatars; - -import com.cloudbees.plugins.credentials.common.StandardCredentials; -import java.awt.image.BufferedImage; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.imageio.ImageIO; - -/** - * - * Basic URL based Cache Source - Replacement for original functionality - * - */ -public class UrlAvatarCacheSource implements AvatarCacheSource { - /** - * Our logger. - */ - private static final Logger LOGGER = Logger.getLogger(UrlAvatarCacheSource.class.getName()); - - private final String url; - - public UrlAvatarCacheSource(String url) { - this.url = url; - } - - @Override - public boolean canFetch() { - return (url != null && (url.startsWith("http://") || url.startsWith("https://"))); - } - - @Override - public AvatarImage fetch(StandardCredentials credentials) { - LOGGER.log(Level.FINE, "Attempting to fetch remote avatar: {0}", url); - long start = System.nanoTime(); - try { - if (!canFetch()) { - LOGGER.log(Level.FINE, "Unable to fetch remote avatar: {0}", url); - return AvatarImage.EMPTY; - } - HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); - try { - connection.setConnectTimeout(10000); - connection.setReadTimeout(30000); - if (connection.getResponseCode() >= 400) { - LOGGER.log(Level.FINE, "Got invalid content response {1} for remote avatar image from {0}", - new String[] { url, String.valueOf(connection.getResponseCode()) }); - return AvatarImage.EMPTY; - } - String contentType = connection.getContentType(); - if (contentType == null || !contentType.startsWith("image/")) { - LOGGER.log(Level.FINE, "Got invalid content type {1} for remote avatar image from {0}", - new String[] { url, contentType }); - return AvatarImage.EMPTY; - } - int length = connection.getContentLength(); - // buffered stream should be no more than 16k if we know the length - // if we don't know the length then 8k is what we will use - length = length > 0 ? Math.min(16384, length) : 8192; - - try (InputStream is = connection.getInputStream(); - BufferedInputStream bis = new BufferedInputStream(is, length)) { - BufferedImage image = ImageIO.read(bis); - if (image == null) { - LOGGER.log(Level.FINE, "Got no remote avatar image from {0}", url); - return AvatarImage.EMPTY; - } - return new AvatarImage(image, connection.getLastModified()); - } catch (Exception e) { - LOGGER.log(Level.INFO, "1:" + e.getMessage(), e); - } - } catch (Exception e) { - LOGGER.log(Level.INFO, "2:" + e.getMessage(), e); - } finally { - connection.disconnect(); - } - } catch (IOException e) { - LOGGER.log(Level.INFO, e.getMessage(), e); - return AvatarImage.EMPTY; - } finally { - long end = System.nanoTime(); - long duration = TimeUnit.NANOSECONDS.toMillis(end - start); - LOGGER.log(duration > 250 ? Level.INFO : Level.FINE, "Avatar lookup of {0} took {1}ms", - new Object[] { url, duration }); - } - return AvatarImage.EMPTY; - } - - @Override - public AvatarImage fetch() { - return fetch(null); - } - - @Override - public String hashKey() { - // TODO Auto-generated method stub - return this.url; - } -} 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 0b3311426..5ef2eb7c5 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 @@ -33,7 +33,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketWebHook; -import com.cloudbees.jenkins.plugins.bitbucket.avatars.AvatarCacheSource.AvatarImage; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudCommit; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestCommits; @@ -72,6 +71,7 @@ import java.util.logging.Level; import javax.imageio.ImageIO; import jenkins.scm.api.SCMFile; +import jenkins.scm.impl.avatars.AvatarImage; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpHost; import org.apache.http.HttpStatus; @@ -101,7 +101,6 @@ public class BitbucketCloudApiClient extends AbstractBitbucketApi implements Bit private final String repositoryName; private final boolean enableCache; private static final Cache cachedTeam = new Cache<>(6, HOURS); - private static final Cache cachedAvatar = new Cache<>(6, HOURS); private static final Cache> cachedRepositories = new Cache<>(3, HOURS); private static final Cache cachedCommits = new Cache<>(24, HOURS); private transient BitbucketRepository cachedRepository; @@ -639,7 +638,7 @@ private BitbucketRepositoryHooks parsePaginatedRepositoryHooks(String response) */ @Override @CheckForNull - public BitbucketTeam getTeam() throws IOException, InterruptedException { + public BitbucketTeam getTeam() throws IOException { final String url = UriTemplate.fromTemplate(V2_WORKSPACES_API_BASE_URL + "{/owner}") .set("owner", owner) .expand(); @@ -669,42 +668,28 @@ public BitbucketTeam getTeam() throws IOException, InterruptedException { /** * {@inheritDoc} */ + @Deprecated @Override @CheckForNull - public AvatarImage getTeamAvatar() throws IOException, InterruptedException { - try { - final BitbucketTeam team = getTeam(); - final String url = (team!=null) ? team.getLink("avatar") : null; - if (url == null) { - return AvatarImage.EMPTY; - } - - Callable request = () -> { - try { - BufferedImage avatar = getImageRequest(url); - return new AvatarImage(avatar, System.currentTimeMillis()); - } catch (FileNotFoundException e) { - logger.log(Level.FINE, "Failed to get avatar for team {0} from URL: " + url, - team.getName()); - } catch (IOException e) { - throw new IOException("I/O error when parsing response from URL: " + url, e); - } - return null; - }; + public AvatarImage getTeamAvatar() throws IOException { + final BitbucketTeam team = getTeam(); + return getAvatar(team == null ? null : team.getAvatar()); + } + @Override + @CheckForNull + public AvatarImage getAvatar(@CheckForNull String url) throws IOException { + if (url != null) { try { - if (enableCache) { - return cachedAvatar.get(owner, request); - } else { - return request.call(); - } - } catch (Exception ex) { - return null; + BufferedImage avatar = getImageRequest(url); + return new AvatarImage(avatar, System.currentTimeMillis()); + } catch (FileNotFoundException e) { + logger.log(Level.FINE, "Failed to get avatar from URL {0}", url); + } catch (IOException e) { + throw new IOException("I/O error when parsing response from URL: " + url, e); } - } catch (Exception ex) { - logger.log(Level.FINE, "Unexpected exception while loading team avatar: "+ex.getMessage(), ex); - throw ex; } + return AvatarImage.EMPTY; } /** diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudRepository.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudRepository.java index b771b437e..9c35acaf4 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudRepository.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudRepository.java @@ -90,6 +90,7 @@ public void setProject(BitbucketProject project) { this.project = project; } + @Override public BitbucketProject getProject() { return this.project; } @@ -133,6 +134,7 @@ public void setPrivate(Boolean priv) { this.priv = priv; } + @Override @JsonIgnore public Map> getLinks() { if (links == null) { @@ -158,4 +160,9 @@ public void setLinks(Map> links) { } } } + + @Override + public String getAvatar() { + return getLink("avatar"); + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java index c4d3558cb..702729aec 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/endpoints/AbstractBitbucketEndpoint.java @@ -221,10 +221,10 @@ public final String getCredentialsId() { @CheckForNull public StandardCredentials credentials() { return StringUtils.isBlank(credentialsId) ? null : CredentialsMatchers.firstOrNull( - CredentialsProvider.lookupCredentials( + CredentialsProvider.lookupCredentialsInItemGroup( StandardCredentials.class, Jenkins.get(), - ACL.SYSTEM, + ACL.SYSTEM2 , URIRequirementBuilder.fromUri(getServerUrl()).build() ), CredentialsMatchers.allOf( diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudTeam.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/BitbucketPlugin.java similarity index 50% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudTeam.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/BitbucketPlugin.java index 5654db7a9..eff857484 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/client/repository/BitbucketCloudTeam.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/BitbucketPlugin.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2016-2017, CloudBees, Inc. + * Copyright (c) 2025, Nikolas Falco * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,17 +21,25 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket.client.repository; +package com.cloudbees.jenkins.plugins.bitbucket.impl; -import com.cloudbees.jenkins.plugins.bitbucket.api.AbstractBitbucketTeam; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketRepoAvatarMetadataAction; +import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketTeamAvatarMetadataAction; +import hudson.init.InitMilestone; +import hudson.init.Initializer; +import hudson.model.Items; +import jenkins.plugins.git.MergeWithGitSCMExtension; -public class BitbucketCloudTeam extends AbstractBitbucketTeam { +public class BitbucketPlugin { - @JsonProperty("username") - protected String name; - - @JsonProperty("display_name") - protected String displayName; + /** + * Mapping classes after refactoring for backward compatibility. + */ + @Initializer(before = InitMilestone.PLUGINS_STARTED) + public static void aliases() { + Items.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.plugins.bitbucket.MergeWithGitSCMExtension", MergeWithGitSCMExtension.class); + Items.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.plugins.bitbucket.BitbucketTeamMetadataAction", BitbucketTeamAvatarMetadataAction.class); + Items.XSTREAM2.addCompatibilityAlias("com.cloudbees.jenkins.plugins.bitbucket.BitbucketRepoMetadataAction", BitbucketRepoAvatarMetadataAction.class); + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketAvatarImageSource.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketAvatarImageSource.java new file mode 100644 index 000000000..d24005121 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketAvatarImageSource.java @@ -0,0 +1,92 @@ +/* + * The MIT License + * + * Copyright (c) 2025, Nikolas Falco + * + * 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 com.cloudbees.jenkins.plugins.bitbucket.impl.avatars; + +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApiFactory; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketAuthenticator; +import com.cloudbees.jenkins.plugins.bitbucket.impl.util.BitbucketCredentials; +import com.cloudbees.plugins.credentials.common.StandardCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.authentication.tokens.api.AuthenticationTokens; +import jenkins.model.Jenkins; +import jenkins.scm.api.SCMNavigatorOwner; +import jenkins.scm.impl.avatars.AvatarImage; +import jenkins.scm.impl.avatars.AvatarImageSource; + +public class BitbucketAvatarImageSource implements AvatarImageSource { + private static final Logger logger = Logger.getLogger(BitbucketAvatarImageSource.class.getName()); + + private final String avatarURL; + private final String serverURL; + private final String credentialsId; + private final String scmOwner; + private transient boolean fetchFailed = false; // NOSONAR, class not implements Serializable but the AvatarCache(.cache) is an action that should be persisted + + + public BitbucketAvatarImageSource(@NonNull String avatarURL, @NonNull String serverURL, @NonNull String scmOwner, @Nullable String credentialsId) { + this.avatarURL = avatarURL; + this.serverURL = serverURL; + this.scmOwner = scmOwner; + this.credentialsId = credentialsId; + } + + @Override + public AvatarImage fetch() { + try { + if (canFetch()) { + SCMNavigatorOwner owner = Jenkins.get().getItemByFullName(scmOwner, SCMNavigatorOwner.class); + if (owner != null) { + StandardCredentials credentials = BitbucketCredentials.lookupCredentials(serverURL, owner, credentialsId, StandardCredentials.class); + BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverURL), credentials); + // projectKey and repository are not used to fetch the project avatar + // owner can not be null but is not used from the client to retrieve avatar image, we just need authentication + try (BitbucketApi client = BitbucketApiFactory.newInstance(serverURL, authenticator, "tmp", null, null)) { + return client.getAvatar(avatarURL); + } + } else { + logger.log(Level.WARNING, "Item {0} seems to be relocated, perform a 'Scan project Now' action to refresh old data", new Object[] { scmOwner }); + } + } + } catch (Exception e) { + logger.log(Level.WARNING, e, () -> "Fail to fetch avatar image for " + serverURL + " using credentialsId " + credentialsId); + fetchFailed = true; // do not retry with same serverURL/credentialsId until Jenkins restarts + } + return AvatarImage.EMPTY; + } + + @Override + public String getId() { + return credentialsId + "@" + avatarURL; + } + + @Override + public boolean canFetch() { + return !fetchFailed && avatarURL != null && serverURL != null; + } + +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketRepoMetadataAction.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketRepoAvatarMetadataAction.java similarity index 62% rename from src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketRepoMetadataAction.java rename to src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketRepoAvatarMetadataAction.java index 9b266a638..c8b23e56b 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketRepoMetadataAction.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketRepoAvatarMetadataAction.java @@ -21,25 +21,34 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package com.cloudbees.jenkins.plugins.bitbucket; +package com.cloudbees.jenkins.plugins.bitbucket.impl.avatars; +import com.cloudbees.jenkins.plugins.bitbucket.Messages; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRepository; import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Util; import java.util.Objects; import jenkins.scm.api.metadata.AvatarMetadataAction; +import jenkins.scm.impl.avatars.AvatarCache; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * Invisible property that retains information about Bitbucket repository. */ -public class BitbucketRepoMetadataAction extends AvatarMetadataAction{ +public class BitbucketRepoAvatarMetadataAction extends AvatarMetadataAction { + private static final long serialVersionUID = 6159334180425135341L; private final String scm; + private String avatarURL; - public BitbucketRepoMetadataAction(@NonNull BitbucketRepository repo) { + public BitbucketRepoAvatarMetadataAction(@NonNull BitbucketRepository repo) { this(repo.getScm()); + this.avatarURL = repo.getAvatar(); } - public BitbucketRepoMetadataAction(String scm) { + @DataBoundConstructor + public BitbucketRepoAvatarMetadataAction(String scm) { this.scm = scm; } @@ -47,11 +56,36 @@ public String getScm() { return scm; } + + public String getAvatarURL() { + return avatarURL; + } + + @DataBoundSetter + public void setAvatarURL(String avatarURL) { + this.avatarURL = Util.fixEmptyAndTrim(avatarURL); + } + + /** + * {@inheritDoc} + */ + @Override + public String getAvatarImageOf(String size) { + if (avatarURL == null) { + return super.getAvatarImageOf(size); + } else { + return AvatarCache.buildUrl(avatarURL, size); + } + } + /** * {@inheritDoc} */ @Override public String getAvatarIconClassName() { + if (avatarURL != null) { + return null; // trigger #getAvatarImageOf(String) if this class override #getAvatarImageOf(String) + } if ("git".equals(scm)) { return "icon-bitbucket-repo-git"; } @@ -69,9 +103,6 @@ public String getAvatarDescription() { return Messages.BitbucketRepoMetadataAction_IconDescription(); } - /** - * {@inheritDoc} - */ @Override public boolean equals(Object o) { if (this == o) { @@ -81,26 +112,14 @@ public boolean equals(Object o) { return false; } - BitbucketRepoMetadataAction that = (BitbucketRepoMetadataAction) o; - - return Objects.equals(scm, that.scm); + BitbucketRepoAvatarMetadataAction that = (BitbucketRepoAvatarMetadataAction) o; + return Objects.equals(scm, that.scm) + && Objects.equals(avatarURL, that.avatarURL); } - /** - * {@inheritDoc} - */ @Override public int hashCode() { - return Objects.hashCode(scm); + return Objects.hash(scm, avatarURL); } - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return "BitbucketRepoMetadataAction{" + - "scm='" + scm + '\'' + - '}'; - } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketTeamAvatarMetadataAction.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketTeamAvatarMetadataAction.java new file mode 100644 index 000000000..e95b7e097 --- /dev/null +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/avatars/BitbucketTeamAvatarMetadataAction.java @@ -0,0 +1,101 @@ +/* + * The MIT License + * + * Copyright (c) 2016, CloudBees, Inc. + * + * 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 com.cloudbees.jenkins.plugins.bitbucket.impl.avatars; + +import com.cloudbees.jenkins.plugins.bitbucket.Messages; +import hudson.Util; +import java.util.Objects; +import jenkins.scm.api.metadata.AvatarMetadataAction; +import jenkins.scm.impl.avatars.AvatarCache; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Invisible property that retains information about the Bitbucket team avatar. + */ +public class BitbucketTeamAvatarMetadataAction extends AvatarMetadataAction { + private static final long serialVersionUID = -7472619697440514373L; + + private final String avatarURL; + private final String serverURL; + private final String scmOwner; + private final String credentialsId; + + @DataBoundConstructor + public BitbucketTeamAvatarMetadataAction(String avatarURL, String serverURL, String scmOwner, String credentialsId) { + this.avatarURL = Util.fixEmptyAndTrim(avatarURL); + this.serverURL = Util.fixEmptyAndTrim(serverURL); + this.scmOwner = Util.fixEmptyAndTrim(scmOwner); + this.credentialsId = Util.fixEmptyAndTrim(credentialsId); + } + + /** + * {@inheritDoc} + */ + @Override + public String getAvatarImageOf(String size) { + if (avatarURL == null) { + return super.getAvatarImageOf(size); + } else { + return AvatarCache.buildUrl(new BitbucketAvatarImageSource(avatarURL, serverURL, scmOwner, credentialsId), size); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getAvatarIconClassName() { + return avatarURL == null ? "icon-bitbucket-logo" : null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getAvatarDescription() { + return Messages.BitbucketTeamMetadataAction_IconDescription(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + BitbucketTeamAvatarMetadataAction other = (BitbucketTeamAvatarMetadataAction) obj; + return Objects.equals(avatarURL, other.avatarURL) + && Objects.equals(serverURL, other.serverURL) + && Objects.equals(credentialsId, other.credentialsId); + } + + @Override + public int hashCode() { + return Objects.hash(avatarURL, serverURL, credentialsId); + } +} diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java index 4238ee993..fc9fb3e62 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/impl/client/AbstractBitbucketApi.java @@ -328,10 +328,8 @@ protected String deleteRequest(String path) throws IOException { } @Override - public void close() throws Exception { - if (getClient() != null) { - getClient().close(); - } + public void close() throws IOException { + getClient().close(); } protected BitbucketAuthenticator getAuthenticator() { 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 ed70669d1..70caf3c84 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 @@ -35,7 +35,6 @@ 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.avatars.AvatarCacheSource.AvatarImage; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.UserRoleInRepository; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketEndpointConfiguration; import com.cloudbees.jenkins.plugins.bitbucket.endpoints.BitbucketServerEndpoint; @@ -87,6 +86,7 @@ import javax.imageio.ImageIO; import jenkins.scm.api.SCMFile; import jenkins.scm.api.SCMFile.Type; +import jenkins.scm.impl.avatars.AvatarImage; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpHost; @@ -808,26 +808,33 @@ public BitbucketTeam getTeam() throws IOException, InterruptedException { } /** - * Get Team avatar + * {@inheritDoc} */ + @Deprecated @Override public AvatarImage getTeamAvatar() throws IOException { if (userCentric) { - return null; + return AvatarImage.EMPTY; } else { String url = UriTemplate.fromTemplate(this.baseURL + AVATAR_PATH) .set("owner", getOwner()) .expand(); - try { - BufferedImage response = getImageRequest(url); - return new AvatarImage(response, System.currentTimeMillis()); - } catch (FileNotFoundException e) { - return null; - } catch (IOException e) { - throw new IOException("I/O error when accessing URL: " + url, e); - } catch (InterruptedException e) { - throw new IOException("InterruptedException when accessing URL: " + url, e); - } + return getAvatar(url); + } + } + + /** + * {@inheritDoc} + */ + @Override + public AvatarImage getAvatar(@NonNull String url) throws IOException { + try { + BufferedImage response = getImageRequest(url); + return new AvatarImage(response, System.currentTimeMillis()); + } catch (FileNotFoundException e) { + return AvatarImage.EMPTY; + } catch (IOException e) { + throw new IOException("I/O error when accessing URL: " + url, e); } } @@ -930,7 +937,7 @@ private V getResource(UriTemplate template, Class> links; + + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + @JsonIgnore + public Map> getLinks() { + return links; + } + + @JsonIgnore + public void setLinks(Map> links) { + this.links = links; + } + + @Override + public String getAvatar() { + return avatar == null ? getLink("self") + "/avatar.png" : avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } } diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerRepository.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerRepository.java index eed5faba8..5264d76a1 100644 --- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerRepository.java +++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerRepository.java @@ -117,6 +117,7 @@ public void setPublic(Boolean isPublic) { this.isPublic = isPublic; } + @Override @JsonIgnore public Map> getLinks() { if (links == null) { @@ -143,4 +144,9 @@ public void setLinks(Map> links) { } } + @Override + public String getAvatar() { + // repository does not supports avatar + return null; + } } diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java index 626ba96ef..db11264ca 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketClientMockUtils.java @@ -24,7 +24,9 @@ package com.cloudbees.jenkins.plugins.bitbucket; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketApi; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketCloudWorkspace; import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref; +import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam; import com.cloudbees.jenkins.plugins.bitbucket.client.BitbucketCloudApiClient; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudAuthor; import com.cloudbees.jenkins.plugins.bitbucket.client.branch.BitbucketCloudBranch; @@ -33,7 +35,6 @@ import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValueDestination; import com.cloudbees.jenkins.plugins.bitbucket.client.pullrequest.BitbucketPullRequestValueRepository; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudRepository; -import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketCloudTeam; import com.cloudbees.jenkins.plugins.bitbucket.client.repository.BitbucketRepositoryHook; import com.cloudbees.jenkins.plugins.bitbucket.hooks.BitbucketSCMSourcePushHookReceiver; import hudson.model.TaskListener; @@ -45,7 +46,7 @@ import java.util.List; import jenkins.model.Jenkins; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -147,11 +148,10 @@ private static List getRepositories() { return Arrays.asList(r1, r2, r3); } - private static BitbucketCloudTeam getTeam() { - BitbucketCloudTeam t = new BitbucketCloudTeam(); - t.setName("myteam"); - t.setDisplayName("This is my team"); - return t; + private static BitbucketTeam getTeam() { + BitbucketCloudWorkspace team = new BitbucketCloudWorkspace(); + team.setName("myteam"); + return team; } private static void withMockGitRepos(BitbucketApi bitbucket) throws IOException, InterruptedException { diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/BitbucketTeamAvatarMetadataActionTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/BitbucketTeamAvatarMetadataActionTest.java new file mode 100644 index 000000000..19de3b523 --- /dev/null +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/BitbucketTeamAvatarMetadataActionTest.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * + * Copyright (c) 2018, Nikolas Falco + * + * 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 com.cloudbees.jenkins.plugins.bitbucket.avatars; + +import com.cloudbees.jenkins.plugins.bitbucket.impl.BitbucketPlugin; +import com.cloudbees.jenkins.plugins.bitbucket.impl.avatars.BitbucketTeamAvatarMetadataAction; +import hudson.model.Items; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class BitbucketTeamAvatarMetadataActionTest { + + @Test + void test_deserialisation() { + BitbucketPlugin.aliases(); + + Object state = Items.XSTREAM2.fromXML(this.getClass().getResource("state.xml")); + assertThat(state) + .isNotNull() + .hasFieldOrProperty("actions") + .extracting("actions") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .hasSize(1) + .extractingByKey("com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::amuniz") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasAtLeastOneElementOfType(BitbucketTeamAvatarMetadataAction.class); + } +} diff --git a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest.java b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest.java index 343eff378..2455e6729 100644 --- a/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest.java +++ b/src/test/java/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest.java @@ -109,7 +109,7 @@ private void resetAudit(@NonNull BitbucketApi client) { void get_repository_parse_correctly_date_from_cloud() throws Exception { BitbucketCloudRepository repository = JsonParser.toJava(loadPayload("getRepository"), BitbucketCloudRepository.class); assertThat(repository.getUpdatedOn()).describedAs("update on date is null").isNotNull(); - Date expectedDate = DateUtils.getDate(2018, 4, 27, 15, 32, 8, 356); + Date expectedDate = DateUtils.getDate(2025, 1, 27, 14, 15, 58, 600); assertThat(repository.getUpdatedOn()).isEqualTo(expectedDate); } diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/avatars/state.xml b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/avatars/state.xml new file mode 100644 index 000000000..4d69eef3d --- /dev/null +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/avatars/state.xml @@ -0,0 +1,32 @@ + + + + + com.cloudbees.jenkins.plugins.bitbucket.BitbucketSCMNavigator::https://bitbucket.org::amuniz + + + amuniz + https://bitbucket.org/amuniz/ + + + + https://bitbucket.org + + GLOBAL + bitbucket.oauth + Bitbucket Cloud OAuth credentials + xxxxxxxxxxxxxxxxxx + {yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy} + true + + amuniz + + + + icon-bitbucket-logo + https://bitbucket.org/amuniz/ + + + + + \ No newline at end of file diff --git a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest/getRepositoryPayload.json b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest/getRepositoryPayload.json index af5c89e55..4703b4335 100644 --- a/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest/getRepositoryPayload.json +++ b/src/test/resources/com/cloudbees/jenkins/plugins/bitbucket/client/BitbucketCloudApiClientTest/getRepositoryPayload.json @@ -1,104 +1,128 @@ { - "scm": "git", - "website": "", - "has_wiki": false, - "uuid": "{ff487924-8721-4ab7-8d5c-e482d4b0eb13}", + "type": "repository", + "full_name": "nfalco79/test-repos", "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos" + }, + "html": { + "href": "https://bitbucket.org/nfalco79/test-repos" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B3deb8c29-778a-450c-8f69-3e50a18079df%7D?ts=3693474" + }, + "pullrequests": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/pullrequests" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/commits" + }, + "forks": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/forks" + }, "watchers": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/watchers" + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/watchers" }, "branches": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/refs/branches" + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/refs/branches" }, "tags": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/refs/tags" + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/refs/tags" }, - "commits": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/commits" + "downloads": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/downloads" + }, + "source": { + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/src" }, "clone": [ { - "href": "https://nfalco79@bitbucket.org/acme/tds.cm.maven.plugins-java.git", - "name": "https" + "name": "https", + "href": "https://opensoftwaresrl@bitbucket.org/nfalco79/test-repos.git" }, { - "href": "git@bitbucket.org:acme/tds.cm.maven.plugins-java.git", - "name": "ssh" + "name": "ssh", + "href": "git@bitbucket.org:nfalco79/test-repos.git" } ], - "self": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java" - }, - "source": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/src" - }, - "html": { - "href": "https://bitbucket.org/acme/tds.cm.maven.plugins-java" - }, - "avatar": { - "href": "https://bitbucket.org/acme/tds.cm.maven.plugins-java/avatar/32/" - }, "hooks": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/hooks" - }, - "forks": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/forks" - }, - "downloads": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/downloads" - }, - "pullrequests": { - "href": "https://api.bitbucket.org/2.0/repositories/acme/tds.cm.maven.plugins-java/pullrequests" + "href": "https://api.bitbucket.org/2.0/repositories/nfalco79/test-repos/hooks" } }, - "fork_policy": "no_forks", - "name": "tds.cm.maven.plugins-java", - "project": { - "key": "THIRDPL", - "type": "project", - "uuid": "{645d71c9-e01c-40ab-976c-3992b9b695a4}", + "name": "test-repos", + "slug": "test-repos", + "description": "", + "scm": "git", + "website": null, + "owner": { + "display_name": "Nikolas Falco", "links": { "self": { - "href": "https://api.bitbucket.org/2.0/teams/acme/projects/THIRDPL" - }, - "html": { - "href": "https://bitbucket.org/account/user/acme/projects/THIRDPL" + "href": "https://api.bitbucket.org/2.0/users/%7B7d3a178a-a087-4756-b2da-2f9eadf50ba8%7D" }, "avatar": { - "href": "https://bitbucket.org/account/user/acme/projects/THIRDPL/avatar/32" + "href": "https://secure.gravatar.com/avatar/9979052fd773fbc9c0d94be07bbc8b5d?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FNF-3.png" + }, + "html": { + "href": "https://bitbucket.org/%7B7d3a178a-a087-4756-b2da-2f9eadf50ba8%7D/" } }, - "name": "3rd Party Libraries" + "type": "user", + "uuid": "{7d3a178a-a087-4756-b2da-2f9eadf50ba8}", + "account_id": "557058:270a1f96-cd27-4013-ade6-85df2ab9820c", + "nickname": "Nikolas Falco" }, - "language": "java", - "created_on": "2016-08-17T11:00:23.063614+00:00", - "mainbranch": { - "type": "branch", - "name": "main" + "workspace": { + "type": "workspace", + "uuid": "{7d3a178a-a087-4756-b2da-2f9eadf50ba8}", + "name": "Nikolas Falco", + "slug": "nfalco79", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/nfalco79/avatar/?ts=1737924067" + }, + "html": { + "href": "https://bitbucket.org/nfalco79/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/nfalco79" + } + } }, - "full_name": "acme/tds.cm.maven.plugins-java", - "has_issues": false, - "owner": { - "username": "acme", - "display_name": "ACME", - "type": "team", - "uuid": "{7f41da2d-686c-4187-9b70-9eb5f71368a6}", + "is_private": false, + "project": { + "type": "project", + "key": "PUB", + "uuid": "{ef731d07-06e0-46d2-9b56-2674649b0655}", + "name": "public", "links": { "self": { - "href": "https://api.bitbucket.org/2.0/teams/acme" + "href": "https://api.bitbucket.org/2.0/workspaces/nfalco79/projects/PUB" }, "html": { - "href": "https://bitbucket.org/acme/" + "href": "https://bitbucket.org/nfalco79/workspace/projects/PUB" }, "avatar": { - "href": "https://bitbucket.org/account/acme/avatar/32/" + "href": "https://bitbucket.org/nfalco79/workspace/projects/PUB/avatar/32?ts=1644525770" } } }, - "updated_on": "2018-04-27T15:32:08.356155+00:00", - "size": 136381135, - "type": "repository", - "slug": "tds.cm.maven.plugins-java", - "is_private": true, - "description": "The acme maven plugins for build system" -} + "fork_policy": "allow_forks", + "created_on": "2018-09-20T12:49:08.541926+00:00", + "updated_on": "2025-01-27T14:15:58.600540+00:00", + "size": 284698, + "language": "", + "uuid": "{3deb8c29-778a-450c-8f69-3e50a18079df}", + "mainbranch": { + "name": "master", + "type": "branch" + }, + "override_settings": { + "default_merge_strategy": false, + "branching_model": false + }, + "parent": null, + "enforced_signed_commits": null, + "has_issues": false, + "has_wiki": false +} \ No newline at end of file