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 all 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{" +

Check warning on line 61 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketCloudWorkspaceAvatarAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 21-61 are not covered by tests
"avatar='" + avatar + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -569,8 +569,10 @@
String defaultTeamUrl;
if (team instanceof BitbucketServerProject) {
defaultTeamUrl = serverUrl + "/projects/" + team.getName();
result.add(new BitbucketServerProjectAvatarAction(owner, this));

Check warning on line 572 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketSCMNavigator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 572 is not covered by tests
} 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 @@ -579,7 +581,6 @@
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 @@ -589,7 +590,6 @@
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;

Check warning on line 125 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketServerProjectAvatarAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 30-125 are not covered by tests

Check warning on line 125 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/BitbucketServerProjectAvatarAction.java

View check run for this annotation

ci.jenkins.io / SpotBugs

SE_NO_SERIALVERSIONID

NORMAL: com.cloudbees.jenkins.plugins.bitbucket.BitbucketServerProjectAvatarAction is Serializable; consider declaring a serialVersionUID
Raw output
<p> This class implements the <code>Serializable</code> interface, but does not define a <code>serialVersionUID</code> field.&nbsp; A change as simple as adding a reference to a .class object will add synthetic fields to the class, which will unfortunately change the implicit serialVersionUID (e.g., adding a reference to <code>String.class</code> will generate a static field <code>class$java$lang$String</code>). Also, different source code to bytecode compilers may use different naming conventions for synthetic variables generated for references to class objects or inner classes. To ensure interoperability of Serializable across versions, consider adding an explicit serialVersionUID.</p>
}

}
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);

Check warning on line 45 in src/main/java/com/cloudbees/jenkins/plugins/bitbucket/avatars/AvatarCacheSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 45 is not covered by tests
}
}

Expand Down
Loading
Loading