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 extends BitbucketRepository> 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 extends PagedApiResponse
return null;
}
- private BufferedImage getImageRequest(String path) throws IOException, InterruptedException {
+ private BufferedImage getImageRequest(String path) throws IOException {
try (InputStream inputStream = getRequestAsInputStream(path)) {
int length = MAX_AVATAR_LENGTH;
BufferedInputStream bis = new BufferedInputStream(inputStream, length);
diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java
index ca3c7c777..45debcd01 100644
--- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/BitbucketServerApiFactory.java
@@ -42,11 +42,14 @@ protected boolean isMatch(@Nullable String serverUrl) {
@NonNull
@Override
- protected BitbucketApi create(@Nullable String serverUrl, @Nullable BitbucketAuthenticator authenticator,
- @NonNull String owner, @CheckForNull String projectKey, @CheckForNull String repository) {
- if(StringUtils.isBlank(serverUrl)){
+ protected BitbucketApi create(@Nullable String serverURL,
+ @Nullable BitbucketAuthenticator authenticator,
+ @NonNull String owner,
+ @CheckForNull String projectKey,
+ @CheckForNull String repository) {
+ if (StringUtils.isBlank(serverURL)) {
throw new IllegalArgumentException("serverUrl is required");
}
- return new BitbucketServerAPIClient(serverUrl, owner, repository, authenticator, false);
+ return new BitbucketServerAPIClient(serverURL, owner, repository, authenticator, false);
}
}
diff --git a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerProject.java b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerProject.java
index fd7d5e7c8..3c7371be7 100644
--- a/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerProject.java
+++ b/src/main/java/com/cloudbees/jenkins/plugins/bitbucket/server/client/repository/BitbucketServerProject.java
@@ -23,15 +23,64 @@
*/
package com.cloudbees.jenkins.plugins.bitbucket.server.client.repository;
-import com.cloudbees.jenkins.plugins.bitbucket.api.AbstractBitbucketTeam;
+import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketHref;
+import com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketTeam;
+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;
-public class BitbucketServerProject extends AbstractBitbucketTeam {
+public class BitbucketServerProject implements BitbucketTeam {
@JsonProperty("key")
- protected String name;
+ private String name;
@JsonProperty("name")
- protected String displayName;
+ private String displayName;
+ private String avatar;
+
+ @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
+ 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