Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avatar metadata for BitBucket organization folder is reworked #700

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.cloudbees.jenkins.plugins.bitbucket;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.Objects;
import jenkins.scm.api.metadata.AvatarMetadataAction;

/**
* Avatar link returned by <a href="https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-workspaces-workspace-get">Get Workspace</a> Bitbucket REST API is public at the moment of writing. Hence, reusing using SCM API plugin provided APIs.
*/
public class BitbucketCloudWorkspaceAvatarAction extends AvatarMetadataAction {
@CheckForNull
private String avatar;

public BitbucketCloudWorkspaceAvatarAction(@CheckForNull String avatar) {
this.avatar = avatar;
}

@Override
public String getAvatarIconClassName() {
if (avatar == null) {
return "icon-bitbucket-scm-navigator";
}
return null;
}

@Override
public String getAvatarDescription() {
return Messages.BitbucketCloudWorkspaceAvatarMetadataAction_IconDescription();
}

@Override
public String getAvatarImageOf(@NonNull String size) {
if (avatar != null) {
return cachedResizedImageOf(avatar, size);
}
return null;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

BitbucketCloudWorkspaceAvatarAction that = (BitbucketCloudWorkspaceAvatarAction) o;

return Objects.equals(avatar, that.avatar);
}

@Override
public int hashCode() {
return Objects.hashCode(avatar);
}

@Override
public String toString() {
return "BitbucketCloudWorkspaceAvatarAction{" +
"avatar='" + avatar + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,10 @@ public List<Action> retrieveActions(@NonNull SCMNavigatorOwner owner,
String defaultTeamUrl;
if (team instanceof BitbucketServerProject) {
defaultTeamUrl = serverUrl + "/projects/" + team.getName();
result.add(new BitbucketServerProjectAvatarAction(owner, this));
} else {
defaultTeamUrl = serverUrl + "/" + team.getName();
result.add(new BitbucketCloudWorkspaceAvatarAction(team.getLink("avatar")));
}
String teamUrl = StringUtils.defaultIfBlank(team.getLink("html"), defaultTeamUrl);
String teamDisplayName = StringUtils.defaultIfBlank(team.getDisplayName(), team.getName());
Expand All @@ -562,7 +564,6 @@ public List<Action> retrieveActions(@NonNull SCMNavigatorOwner owner,
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 {
Expand All @@ -572,7 +573,6 @@ public List<Action> retrieveActions(@NonNull SCMNavigatorOwner owner,
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");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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.plugins.credentials.common.StandardCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Util;
import java.io.IOException;
import java.util.Objects;
import java.util.StringJoiner;
import jenkins.authentication.tokens.api.AuthenticationTokens;
import jenkins.model.Jenkins;
import jenkins.scm.api.SCMNavigatorOwner;
import jenkins.scm.api.metadata.AvatarMetadataAction;
import jenkins.scm.impl.avatars.AvatarCache;
import jenkins.scm.impl.avatars.AvatarImage;
import jenkins.scm.impl.avatars.AvatarImageSource;

public class BitbucketServerProjectAvatarAction extends AvatarMetadataAction implements AvatarImageSource {

// This can change when SCMNavigatorOwner is moved but this Action is only persisted
// when BitbucketSCMNavigator.retrieveActions which, at the moment of writing, happens on webhook events and indexing.
// Hence, implementing ItemListener to monitor of location changes seems impractical.
private String ownerFullName;
private String serverUrl;
private String credentialsId;
private String projectKey;
// owner can be moved around or credential can get blocked or revoked.
private transient boolean canFetch = true;

public BitbucketServerProjectAvatarAction(SCMNavigatorOwner owner, BitbucketSCMNavigator navigator) {
this(owner.getFullName(), navigator.getServerUrl(), navigator.getCredentialsId(), navigator.getRepoOwner());
}

public BitbucketServerProjectAvatarAction(String ownerFullName, String serverUrl, String credentialsId, String projectKey) {
this.ownerFullName = Util.fixEmpty(ownerFullName);
this.serverUrl = Util.fixEmpty(serverUrl);
this.credentialsId = Util.fixEmpty(credentialsId);
this.projectKey = Util.fixEmpty(projectKey);
}

@Override
public String getAvatarIconClassName() {
if (!canFetch()) {
return "icon-bitbucket-scm-navigator";
}
return null;
}

@Override
public String getAvatarDescription() {
return Messages.BitbucketServerProjectAvatarMetadataAction_IconDescription();
}

@Override
public String getAvatarImageOf(@NonNull String size) {
if (canFetch()) {
return AvatarCache.buildUrl(this, size);
}
return null;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BitbucketServerProjectAvatarAction that = (BitbucketServerProjectAvatarAction) o;
return Objects.equals(serverUrl, that.serverUrl) && Objects.equals(credentialsId, that.credentialsId) && Objects.equals(projectKey, that.projectKey) && Objects.equals(ownerFullName, that.ownerFullName);
}

@Override
public int hashCode() {
return Objects.hash(serverUrl, credentialsId, projectKey, ownerFullName);
}

@Override
public AvatarImage fetch() {
if (canFetch()) {
return doFetch();
}
return null;
}

private AvatarImage doFetch() {
SCMNavigatorOwner owner = Jenkins.get().getItemByFullName(ownerFullName, SCMNavigatorOwner.class);
if (owner != null) {
StandardCredentials credentials = BitbucketCredentials.lookupCredentials(
serverUrl,
owner,
credentialsId,
StandardCredentials.class
);

BitbucketAuthenticator authenticator = AuthenticationTokens.convert(BitbucketAuthenticator.authenticationContext(serverUrl), credentials);

BitbucketApi bitbucket = BitbucketApiFactory.newInstance(serverUrl, authenticator, projectKey, null, null);
try {
return bitbucket.getTeamAvatar();
} catch (IOException e) {
canFetch = false;
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
// Owner was probably relocated
canFetch = false;
}
return null;
}

@Override
public String getId() {
return new StringJoiner("::")
.add(serverUrl)
.add(credentialsId)
.add(projectKey)
.add(ownerFullName)
.toString();
}

@Override
public boolean canFetch() {
return canFetch && ownerFullName != null && serverUrl != null && projectKey != null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@

/**
* Invisible property that retains information about Bitbucket team.
* @deprecated Replaced with {@link BitbucketServerProjectAvatarAction} and {@link BitbucketCloudWorkspaceAvatarAction}
*/
@Deprecated
public class BitbucketTeamMetadataAction extends AvatarMetadataAction {
/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@

/**
* An avatar cache that will serve URLs that have been recently registered
* through {@link #buildUrl(String, String)}.
* through {@link #buildUrl(AvatarCacheSource, String)}.
*
* @since 2.2.0
* @deprecated Copy/Paste from SCM API plugin. Use {@link jenkins.scm.impl.avatars.AvatarCache} instead.
*/
@Deprecated
@Extension
public class AvatarCache implements UnprotectedRootAction {

Expand Down Expand Up @@ -128,18 +130,6 @@ public AvatarCache() {
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,20 @@
/**
*
* Interface for Avatar Cache Item Source
*
* @deprecated Copy/Paste from SCM API plugin. Implement {@link jenkins.scm.impl.avatars.AvatarImageSource} instead.
*/
@Deprecated
public interface AvatarCacheSource {

/**
* Holds Image and lastModified date
* @deprecated Copy/Paste from SCM API plugin. Use {@link jenkins.scm.impl.avatars.AvatarImage} directly instead.
*/
public static class AvatarImage {
public final BufferedImage image;
public final long lastModified;

@Deprecated
class AvatarImage extends jenkins.scm.impl.avatars.AvatarImage {

Check notice

Code scanning / CodeQL

Class has same name as super class Note

AvatarImage has the same name as its supertype
jenkins.scm.impl.avatars.AvatarImage
.
public static final AvatarImage EMPTY = new AvatarImage(null, 0);

public AvatarImage(final BufferedImage image, final long lastModified) {
this.image = image;
this.lastModified = lastModified;
public AvatarImage(BufferedImage image, long lastModified) {
super(image, lastModified);
}
}

Expand Down
Loading