From a793f0c5703cb0d34026ba9f5ff7e2763cc4856a Mon Sep 17 00:00:00 2001 From: Nick Cross Date: Tue, 9 Jun 2026 15:14:22 +0100 Subject: [PATCH 1/4] Add upload and promote build --- .../org/jfrog/artifactory/client/Builds.java | 43 +++ .../artifactory/client/model/BuildInfo.java | 248 ++++++++++++++++++ .../client/model/BuildPromotionRequest.java | 100 +++++++ .../client/model/BuildPromotionResponse.java | 21 ++ .../client/model/PromotionMessage.java | 25 ++ .../artifactory/client/impl/BuildsImpl.java | 44 ++++ .../client/model/impl/BuildInfoImpl.java | 218 +++++++++++++++ .../model/impl/BuildPromotionRequestImpl.java | 152 +++++++++++ .../impl/BuildPromotionResponseImpl.java | 26 ++ .../model/impl/PromotionMessageImpl.java | 33 +++ .../jfrog/artifactory/client/BuildsTests.java | 79 +++++- .../org/jfrog/artifactory/client/Utils.java | 15 ++ 12 files changed, 1002 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java create mode 100644 api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java create mode 100644 api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java create mode 100644 api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java create mode 100644 services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java create mode 100644 services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java create mode 100644 services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java create mode 100644 services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java diff --git a/api/src/main/java/org/jfrog/artifactory/client/Builds.java b/api/src/main/java/org/jfrog/artifactory/client/Builds.java index e4b32751..4d0651fa 100644 --- a/api/src/main/java/org/jfrog/artifactory/client/Builds.java +++ b/api/src/main/java/org/jfrog/artifactory/client/Builds.java @@ -1,6 +1,9 @@ package org.jfrog.artifactory.client; import org.jfrog.artifactory.client.model.AllBuilds; +import org.jfrog.artifactory.client.model.BuildInfo; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; import java.io.IOException; @@ -12,4 +15,44 @@ public interface Builds { AllBuilds getAllBuilds() throws IOException; BuildRuns getBuildRuns(String buildName) throws IOException; + + /** + * Upload a build to Artifactory + * + * @param buildInfo the build info + * @throws IOException if the upload fails + */ + void uploadBuild(BuildInfo buildInfo) throws IOException; + + /** + * Upload a build to Artifactory with a project parameter + * + * @param buildInfo the build info + * @param project the project name to limit the build to + * @throws IOException if the upload fails + */ + void uploadBuild(BuildInfo buildInfo, String project) throws IOException; + + /** + * Promote a build in Artifactory + * + * @param buildName the name of the build to promote + * @param buildNumber the number of the build to promote + * @param promotionRequest the promotion request details + * @return the promotion response with messages + * @throws IOException if the promotion fails + */ + BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest) throws IOException; + + /** + * Promote a build in Artifactory with a project parameter + * + * @param buildName the name of the build to promote + * @param buildNumber the number of the build to promote + * @param promotionRequest the promotion request details + * @param project the project name + * @return the promotion response with messages + * @throws IOException if the promotion fails + */ + BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest, String project) throws IOException; } diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java new file mode 100644 index 00000000..63ab15d4 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java @@ -0,0 +1,248 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; +import java.util.Map; + +/** + * Build Info structure for uploading to Artifactory + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface BuildInfo { + /** + * Build Info schema version + * @return the version + */ + String getVersion(); + + /** + * Build name + * @return the build name + */ + String getName(); + + /** + * Build number + * @return the build number + */ + String getNumber(); + + /** + * Build type (MAVEN, GRADLE, ANT, IVY, GENERIC) + * @return the build type + */ + String getType(); + + /** + * Build agent information (build tool) + * @return the build agent + */ + BuildAgent getBuildAgent(); + + /** + * CI agent information (CI server) + * @return the agent + */ + Agent getAgent(); + + /** + * Build start time in ISO 8601 format (yyyy-MM-dd'T'HH:mm:ss.SSSZ) + * @return the start time + */ + String getStarted(); + + /** + * Artifactory plugin version + * @return the plugin version + */ + String getArtifactoryPluginVersion(); + + /** + * Build duration in milliseconds + * @return the duration + */ + Long getDurationMillis(); + + /** + * Artifactory principal (the Artifactory user used for deployment) + * @return the principal + */ + String getArtifactoryPrincipal(); + + /** + * CI server URL + * @return the URL + */ + String getUrl(); + + /** + * VCS revision + * @return the VCS revision + */ + String getVcsRevision(); + + /** + * VCS URL + * @return the VCS URL + */ + String getVcsUrl(); + + /** + * VCS information list + * @return the VCS list + */ + List getVcs(); + + /** + * License control settings + * @return the license control + */ + LicenseControl getLicenseControl(); + + /** + * Build retention settings + * @return the build retention + */ + BuildRetention getBuildRetention(); + + /** + * Build modules + * @return the modules + */ + List getModules(); + + /** + * Issues information + * @return the issues + */ + Issues getIssues(); + + /** + * Governance information + * @return the governance + */ + Map getGovernance(); + + /** + * Environment variables and properties + * @return the properties + */ + Map getProperties(); + + /** + * Build agent (build tool) information + */ + interface BuildAgent { + String getName(); + String getVersion(); + } + + /** + * CI agent (CI server) information + */ + interface Agent { + String getName(); + String getVersion(); + } + + /** + * VCS information + */ + interface VcsInfo { + String getRevision(); + String getMessage(); + String getBranch(); + String getUrl(); + } + + /** + * License control settings + */ + interface LicenseControl { + Boolean getRunChecks(); + Boolean getIncludePublishedArtifacts(); + Boolean getAutoDiscover(); + String getScopesList(); + String getLicenseViolationsRecipientsList(); + } + + /** + * Build retention settings + */ + interface BuildRetention { + Boolean getDeleteBuildArtifacts(); + Integer getCount(); + Long getMinimumBuildDate(); + List getBuildNumbersNotToBeDiscarded(); + } + + /** + * Build module + */ + interface BuildModule { + Map getProperties(); + String getId(); + String getType(); + List getArtifacts(); + List getDependencies(); + } + + /** + * Build artifact + */ + interface Artifact { + String getType(); + String getSha1(); + String getSha256(); + String getMd5(); + String getName(); + String getPath(); + String getOriginalDeploymentRepo(); + } + + /** + * Build dependency + */ + interface Dependency { + String getType(); + String getSha1(); + String getSha256(); + String getMd5(); + String getId(); + List getScopes(); + List> getRequestedBy(); + } + + /** + * Issues information + */ + interface Issues { + Tracker getTracker(); + Boolean getAggregateBuildIssues(); + String getAggregationBuildStatus(); + List getAffectedIssues(); + } + + /** + * Issue tracker information + */ + interface Tracker { + String getName(); + String getVersion(); + } + + /** + * Affected issue + */ + interface AffectedIssue { + String getKey(); + String getUrl(); + String getSummary(); + Boolean getAggregated(); + } +} + +// Made with Bob diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java new file mode 100644 index 00000000..3bb09323 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionRequest.java @@ -0,0 +1,100 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Request for promoting a build in Artifactory + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface BuildPromotionRequest { + /** + * The new status of the build + * @return the status + */ + String getStatus(); + + /** + * An optional comment describing the reason for the promotion + * @return the comment + */ + String getComment(); + + /** + * The user that invoked promotion from the CI server + * @return the CI user + */ + @JsonProperty("ciUser") + String getCiUser(); + + /** + * The time when the promotion command was received by Artifactory (ISO8601 format) + * @return the timestamp + */ + String getTimestamp(); + + /** + * When set to true, performs a dry run of the promotion without executing any operation + * @return true for dry run + */ + @JsonProperty("dryRun") + Boolean getDryRun(); + + /** + * The repository from which the build contents will be copied or moved + * @return the source repository + */ + @JsonProperty("sourceRepo") + String getSourceRepo(); + + /** + * The target repository to which the build contents will be copied or moved + * @return the target repository + */ + @JsonProperty("targetRepo") + String getTargetRepo(); + + /** + * Determines how to perform the build promotion. true = copy, false = move + * @return true to copy, false to move + */ + Boolean getCopy(); + + /** + * Determines whether to move/copy the build's artifacts + * @return true to include artifacts + */ + Boolean getArtifacts(); + + /** + * Determines whether to move/copy the build's dependencies + * @return true to include dependencies + */ + Boolean getDependencies(); + + /** + * An array of dependency scopes + * @return the scopes + */ + List getScopes(); + + /** + * A list of properties to attach to the build's artifacts + * @return the properties + */ + Map getProperties(); + + /** + * When set to true, fails and aborts the promotion operation upon receiving an error + * @return true to fail fast + */ + @JsonProperty("failFast") + Boolean getFailFast(); +} + +// Made with Bob diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java new file mode 100644 index 00000000..80891f61 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/BuildPromotionResponse.java @@ -0,0 +1,21 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +/** + * Response from promoting a build in Artifactory + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface BuildPromotionResponse { + /** + * Get the list of messages from the promotion operation + * @return the messages + */ + List getMessages(); +} + +// Made with Bob diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java b/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java new file mode 100644 index 00000000..c3dc5ff2 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/model/PromotionMessage.java @@ -0,0 +1,25 @@ +package org.jfrog.artifactory.client.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A message returned from a build promotion operation + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public interface PromotionMessage { + /** + * The level of the message (error, warning, info) + * @return the message level + */ + String getLevel(); + + /** + * The message text + * @return the message + */ + String getMessage(); +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java index a6500d92..49044029 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java +++ b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java @@ -1,13 +1,21 @@ package org.jfrog.artifactory.client.impl; +import org.apache.http.entity.ContentType; import org.jfrog.artifactory.client.Artifactory; import org.jfrog.artifactory.client.Builds; +import org.jfrog.artifactory.client.impl.util.Util; import org.jfrog.artifactory.client.model.AllBuilds; +import org.jfrog.artifactory.client.model.BuildInfo; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; import org.jfrog.artifactory.client.model.impl.AllBuildsImpl; +import org.jfrog.artifactory.client.model.impl.BuildPromotionResponseImpl; import org.jfrog.artifactory.client.model.impl.BuildRunsImpl; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; /** * @author yahavi @@ -31,6 +39,42 @@ public BuildRuns getBuildRuns(String buildName) throws IOException { return artifactory.get(getBuilderApi() + buildName, BuildRunsImpl.class, BuildRuns.class); } + @Override + public void uploadBuild(BuildInfo buildInfo) throws IOException { + uploadBuild(buildInfo, null); + } + + @Override + public void uploadBuild(BuildInfo buildInfo, String project) throws IOException { + String apiPath = getBuilderApi(); + if (project != null && !project.isEmpty()) { + apiPath += "?project=" + project; + } + + Map headers = new HashMap<>(); + artifactory.put(apiPath, ContentType.APPLICATION_JSON, + Util.getStringFromObject(buildInfo), headers, null, -1, + String.class, null); + } + + @Override + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest) throws IOException { + return promoteBuild(buildName, buildNumber, promotionRequest, null); + } + + @Override + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest, String project) throws IOException { + String apiPath = getBuilderApi() + "promote/" + buildName + "/" + buildNumber; + if (project != null && !project.isEmpty()) { + apiPath += "?project=" + project; + } + + Map headers = new HashMap<>(); + return artifactory.post(apiPath, ContentType.APPLICATION_JSON, + Util.getStringFromObject(promotionRequest), headers, + BuildPromotionResponseImpl.class, BuildPromotionResponse.class); + } + public String getBuilderApi() { return baseApiPath + "/build/"; } diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java new file mode 100644 index 00000000..087491c3 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java @@ -0,0 +1,218 @@ +package org.jfrog.artifactory.client.model.impl; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.jfrog.artifactory.client.model.BuildInfo; + +import java.util.List; +import java.util.Map; + +/** + * Implementation of BuildInfo + * + * @author rnc + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class BuildInfoImpl implements BuildInfo { + private String version; + private String name; + private String number; + private String type; + private BuildAgent buildAgent; + private Agent agent; + private String started; + private String artifactoryPluginVersion; + private Long durationMillis; + private String artifactoryPrincipal; + private String url; + private String vcsRevision; + private String vcsUrl; + private List vcs; + private LicenseControl licenseControl; + private BuildRetention buildRetention; + private List modules; + private Issues issues; + private Map governance; + private Map properties; + + @Override + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + @Override + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public BuildAgent getBuildAgent() { + return buildAgent; + } + + public void setBuildAgent(BuildAgent buildAgent) { + this.buildAgent = buildAgent; + } + + @Override + public Agent getAgent() { + return agent; + } + + public void setAgent(Agent agent) { + this.agent = agent; + } + + @Override + public String getStarted() { + return started; + } + + public void setStarted(String started) { + this.started = started; + } + + @Override + public String getArtifactoryPluginVersion() { + return artifactoryPluginVersion; + } + + public void setArtifactoryPluginVersion(String artifactoryPluginVersion) { + this.artifactoryPluginVersion = artifactoryPluginVersion; + } + + @Override + public Long getDurationMillis() { + return durationMillis; + } + + public void setDurationMillis(Long durationMillis) { + this.durationMillis = durationMillis; + } + + @Override + public String getArtifactoryPrincipal() { + return artifactoryPrincipal; + } + + public void setArtifactoryPrincipal(String artifactoryPrincipal) { + this.artifactoryPrincipal = artifactoryPrincipal; + } + + @Override + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public String getVcsRevision() { + return vcsRevision; + } + + public void setVcsRevision(String vcsRevision) { + this.vcsRevision = vcsRevision; + } + + @Override + public String getVcsUrl() { + return vcsUrl; + } + + public void setVcsUrl(String vcsUrl) { + this.vcsUrl = vcsUrl; + } + + @Override + public List getVcs() { + return vcs; + } + + public void setVcs(List vcs) { + this.vcs = vcs; + } + + @Override + public LicenseControl getLicenseControl() { + return licenseControl; + } + + public void setLicenseControl(LicenseControl licenseControl) { + this.licenseControl = licenseControl; + } + + @Override + public BuildRetention getBuildRetention() { + return buildRetention; + } + + public void setBuildRetention(BuildRetention buildRetention) { + this.buildRetention = buildRetention; + } + + @Override + public List getModules() { + return modules; + } + + public void setModules(List modules) { + this.modules = modules; + } + + @Override + public Issues getIssues() { + return issues; + } + + public void setIssues(Issues issues) { + this.issues = issues; + } + + @Override + public Map getGovernance() { + return governance; + } + + public void setGovernance(Map governance) { + this.governance = governance; + } + + @Override + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java new file mode 100644 index 00000000..1e583c01 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionRequestImpl.java @@ -0,0 +1,152 @@ +package org.jfrog.artifactory.client.model.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jfrog.artifactory.client.model.BuildPromotionRequest; + +import java.util.List; +import java.util.Map; + +/** + * Implementation of BuildPromotionRequest + * + * @author rnc + */ +public class BuildPromotionRequestImpl implements BuildPromotionRequest { + private String status; + private String comment; + @JsonProperty("ciUser") + private String ciUser; + private String timestamp; + @JsonProperty("dryRun") + private Boolean dryRun; + @JsonProperty("sourceRepo") + private String sourceRepo; + @JsonProperty("targetRepo") + private String targetRepo; + private Boolean copy; + private Boolean artifacts; + private Boolean dependencies; + private List scopes; + private Map properties; + @JsonProperty("failFast") + private Boolean failFast; + + @Override + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @Override + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + @Override + public String getCiUser() { + return ciUser; + } + + public void setCiUser(String ciUser) { + this.ciUser = ciUser; + } + + @Override + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + @Override + public Boolean getDryRun() { + return dryRun; + } + + public void setDryRun(Boolean dryRun) { + this.dryRun = dryRun; + } + + @Override + public String getSourceRepo() { + return sourceRepo; + } + + public void setSourceRepo(String sourceRepo) { + this.sourceRepo = sourceRepo; + } + + @Override + public String getTargetRepo() { + return targetRepo; + } + + public void setTargetRepo(String targetRepo) { + this.targetRepo = targetRepo; + } + + @Override + public Boolean getCopy() { + return copy; + } + + public void setCopy(Boolean copy) { + this.copy = copy; + } + + @Override + public Boolean getArtifacts() { + return artifacts; + } + + public void setArtifacts(Boolean artifacts) { + this.artifacts = artifacts; + } + + @Override + public Boolean getDependencies() { + return dependencies; + } + + public void setDependencies(Boolean dependencies) { + this.dependencies = dependencies; + } + + @Override + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + @Override + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + @Override + public Boolean getFailFast() { + return failFast; + } + + public void setFailFast(Boolean failFast) { + this.failFast = failFast; + } +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java new file mode 100644 index 00000000..36017c87 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java @@ -0,0 +1,26 @@ +package org.jfrog.artifactory.client.model.impl; + +import org.jfrog.artifactory.client.model.BuildPromotionResponse; +import org.jfrog.artifactory.client.model.PromotionMessage; + +import java.util.List; + +/** + * Implementation of BuildPromotionResponse + * + * @author rnc + */ +public class BuildPromotionResponseImpl implements BuildPromotionResponse { + private List messages; + + @Override + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages; + } +} + +// Made with Bob diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java new file mode 100644 index 00000000..b5e3b3a0 --- /dev/null +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/PromotionMessageImpl.java @@ -0,0 +1,33 @@ +package org.jfrog.artifactory.client.model.impl; + +import org.jfrog.artifactory.client.model.PromotionMessage; + +/** + * Implementation of PromotionMessage + * + * @author rnc + */ +public class PromotionMessageImpl implements PromotionMessage { + private String level; + private String message; + + @Override + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} + +// Made with Bob diff --git a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java index c74668a6..d84de0b4 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java +++ b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java @@ -3,19 +3,24 @@ import org.apache.commons.lang3.StringUtils; import org.jfrog.artifactory.client.model.AllBuilds; import org.jfrog.artifactory.client.model.Build; +import org.jfrog.artifactory.client.model.BuildInfo; import org.jfrog.artifactory.client.model.BuildNumber; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; +import org.jfrog.artifactory.client.model.PromotionMessage; +import org.jfrog.artifactory.client.model.impl.BuildPromotionRequestImpl; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import java.io.IOException; +import java.text.SimpleDateFormat; import java.util.List; import java.util.Map; import static org.jfrog.artifactory.client.Utils.createBuildBody; +import static org.jfrog.artifactory.client.Utils.createBuildInfo; import static org.jfrog.artifactory.client.Utils.uploadBuild; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; /** * @author yahavi @@ -23,6 +28,13 @@ public class BuildsTests extends ArtifactoryTestsBase { private static final String BUILDS_API = "/api/build"; + private static final String TEST_BUILD_NAME = "TestBuild"; + private static final String TEST_BUILD_NUMBER = "13"; + private static final String UPLOAD_TEST_BUILD_NAME = "UploadTestBuild"; + private static final String UPLOAD_TEST_BUILD_NUMBER = "100"; + private static final String PROMOTE_TEST_BUILD_NAME = "PromoteTestBuild"; + private static final String PROMOTE_TEST_BUILD_NUMBER = "200"; + private Map buildBody; @BeforeClass @@ -67,6 +79,69 @@ public void testGetBuildRuns() throws IOException { assertTrue(StringUtils.isNotBlank(buildNumber.getStarted())); } + @Test + public void testUploadBuild() throws IOException { + // Create a new build info + BuildInfo buildInfo = createBuildInfo(); + + // Modify the build name and number to avoid conflicts + ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setName(UPLOAD_TEST_BUILD_NAME); + ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setNumber(UPLOAD_TEST_BUILD_NUMBER); + + // Upload the build + artifactory.builds().uploadBuild(buildInfo); + + // Verify the build was uploaded by retrieving it + BuildRuns buildRuns = artifactory.builds().getBuildRuns(UPLOAD_TEST_BUILD_NAME); + assertNotNull(buildRuns); + + // Check that our build number exists + BuildNumber buildNumber = buildRuns.getBuildsNumbers().stream() + .filter(bn -> StringUtils.equals(bn.getUri(), "/" + UPLOAD_TEST_BUILD_NUMBER)) + .findAny().orElse(null); + assertNotNull(buildNumber, "Build number " + UPLOAD_TEST_BUILD_NUMBER + " was not found after upload"); + } + + @Test + public void testPromoteBuild() throws IOException { + // First upload a build to promote + BuildInfo buildInfo = createBuildInfo(); + ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setName(PROMOTE_TEST_BUILD_NAME); + ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setNumber(PROMOTE_TEST_BUILD_NUMBER); + artifactory.builds().uploadBuild(buildInfo); + + // Create promotion request + BuildPromotionRequestImpl promotionRequest = new BuildPromotionRequestImpl(); + promotionRequest.setStatus("Released"); + promotionRequest.setComment("Promoted by automated test"); + promotionRequest.setCiUser("testUser"); + promotionRequest.setTimestamp(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis())); + promotionRequest.setCopy(false); + promotionRequest.setArtifacts(true); + promotionRequest.setDependencies(false); + promotionRequest.setFailFast(true); + promotionRequest.setDryRun(true); // Use dry run to avoid needing actual artifacts + + // Promote the build + BuildPromotionResponse response = artifactory.builds().promoteBuild( + PROMOTE_TEST_BUILD_NAME, + PROMOTE_TEST_BUILD_NUMBER, + promotionRequest + ); + + // Verify response + assertNotNull(response); + assertNotNull(response.getMessages()); + + // In dry run mode, we should get messages about what would happen + if (!response.getMessages().isEmpty()) { + for (PromotionMessage message : response.getMessages()) { + assertNotNull(message.getLevel()); + assertNotNull(message.getMessage()); + } + } + } + private String getExpectedBuildName() { return (String) buildBody.get("name"); } diff --git a/services/src/test/java/org/jfrog/artifactory/client/Utils.java b/services/src/test/java/org/jfrog/artifactory/client/Utils.java index 5db481b1..23ca5f57 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/Utils.java +++ b/services/src/test/java/org/jfrog/artifactory/client/Utils.java @@ -5,6 +5,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jfrog.artifactory.client.impl.ArtifactoryRequestImpl; +import org.jfrog.artifactory.client.model.BuildInfo; +import org.jfrog.artifactory.client.model.impl.BuildInfoImpl; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -40,4 +42,17 @@ public static Map createBuildBody() { } return new HashMap<>(); } + + public static BuildInfo createBuildInfo() { + String buildStarted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis()); + try { + String buildInfoJson = IOUtils.toString(Utils.class.getResourceAsStream("/build.json"), StandardCharsets.UTF_8); + buildInfoJson = StringUtils.replace(buildInfoJson, "{build.start.time}", buildStarted); + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(buildInfoJson, BuildInfoImpl.class); + } catch (IOException e) { + fail(ExceptionUtils.getRootCauseMessage(e)); + } + return null; + } } From 7c17b7704b139ae467e0806020cfae460dc529d8 Mon Sep 17 00:00:00 2001 From: Nick Cross Date: Tue, 9 Jun 2026 16:27:00 +0100 Subject: [PATCH 2/4] Migrate to use official build-info API --- .../org/jfrog/artifactory/client/Builds.java | 14 +- .../artifactory/client/model/BuildInfo.java | 248 ------------------ build.gradle | 1 + .../artifactory/client/impl/BuildsImpl.java | 10 +- .../client/model/impl/BuildInfoImpl.java | 218 --------------- .../jfrog/artifactory/client/BuildsTests.java | 29 +- .../org/jfrog/artifactory/client/Utils.java | 7 +- 7 files changed, 30 insertions(+), 497 deletions(-) delete mode 100644 api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java delete mode 100644 services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java diff --git a/api/src/main/java/org/jfrog/artifactory/client/Builds.java b/api/src/main/java/org/jfrog/artifactory/client/Builds.java index 4d0651fa..e32d2094 100644 --- a/api/src/main/java/org/jfrog/artifactory/client/Builds.java +++ b/api/src/main/java/org/jfrog/artifactory/client/Builds.java @@ -1,10 +1,10 @@ package org.jfrog.artifactory.client; import org.jfrog.artifactory.client.model.AllBuilds; -import org.jfrog.artifactory.client.model.BuildInfo; import org.jfrog.artifactory.client.model.BuildPromotionRequest; import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; +import org.jfrog.build.api.Build; import java.io.IOException; @@ -17,21 +17,21 @@ public interface Builds { BuildRuns getBuildRuns(String buildName) throws IOException; /** - * Upload a build to Artifactory + * Upload a build to Artifactory using the official build-info API * - * @param buildInfo the build info + * @param build the build info from org.jfrog.build.api.Build * @throws IOException if the upload fails */ - void uploadBuild(BuildInfo buildInfo) throws IOException; + void uploadBuild(Build build) throws IOException; /** - * Upload a build to Artifactory with a project parameter + * Upload a build to Artifactory with a project parameter using the official build-info API * - * @param buildInfo the build info + * @param build the build info from org.jfrog.build.api.Build * @param project the project name to limit the build to * @throws IOException if the upload fails */ - void uploadBuild(BuildInfo buildInfo, String project) throws IOException; + void uploadBuild(Build build, String project) throws IOException; /** * Promote a build in Artifactory diff --git a/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java b/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java deleted file mode 100644 index 63ab15d4..00000000 --- a/api/src/main/java/org/jfrog/artifactory/client/model/BuildInfo.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.jfrog.artifactory.client.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; -import java.util.Map; - -/** - * Build Info structure for uploading to Artifactory - * - * @author rnc - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public interface BuildInfo { - /** - * Build Info schema version - * @return the version - */ - String getVersion(); - - /** - * Build name - * @return the build name - */ - String getName(); - - /** - * Build number - * @return the build number - */ - String getNumber(); - - /** - * Build type (MAVEN, GRADLE, ANT, IVY, GENERIC) - * @return the build type - */ - String getType(); - - /** - * Build agent information (build tool) - * @return the build agent - */ - BuildAgent getBuildAgent(); - - /** - * CI agent information (CI server) - * @return the agent - */ - Agent getAgent(); - - /** - * Build start time in ISO 8601 format (yyyy-MM-dd'T'HH:mm:ss.SSSZ) - * @return the start time - */ - String getStarted(); - - /** - * Artifactory plugin version - * @return the plugin version - */ - String getArtifactoryPluginVersion(); - - /** - * Build duration in milliseconds - * @return the duration - */ - Long getDurationMillis(); - - /** - * Artifactory principal (the Artifactory user used for deployment) - * @return the principal - */ - String getArtifactoryPrincipal(); - - /** - * CI server URL - * @return the URL - */ - String getUrl(); - - /** - * VCS revision - * @return the VCS revision - */ - String getVcsRevision(); - - /** - * VCS URL - * @return the VCS URL - */ - String getVcsUrl(); - - /** - * VCS information list - * @return the VCS list - */ - List getVcs(); - - /** - * License control settings - * @return the license control - */ - LicenseControl getLicenseControl(); - - /** - * Build retention settings - * @return the build retention - */ - BuildRetention getBuildRetention(); - - /** - * Build modules - * @return the modules - */ - List getModules(); - - /** - * Issues information - * @return the issues - */ - Issues getIssues(); - - /** - * Governance information - * @return the governance - */ - Map getGovernance(); - - /** - * Environment variables and properties - * @return the properties - */ - Map getProperties(); - - /** - * Build agent (build tool) information - */ - interface BuildAgent { - String getName(); - String getVersion(); - } - - /** - * CI agent (CI server) information - */ - interface Agent { - String getName(); - String getVersion(); - } - - /** - * VCS information - */ - interface VcsInfo { - String getRevision(); - String getMessage(); - String getBranch(); - String getUrl(); - } - - /** - * License control settings - */ - interface LicenseControl { - Boolean getRunChecks(); - Boolean getIncludePublishedArtifacts(); - Boolean getAutoDiscover(); - String getScopesList(); - String getLicenseViolationsRecipientsList(); - } - - /** - * Build retention settings - */ - interface BuildRetention { - Boolean getDeleteBuildArtifacts(); - Integer getCount(); - Long getMinimumBuildDate(); - List getBuildNumbersNotToBeDiscarded(); - } - - /** - * Build module - */ - interface BuildModule { - Map getProperties(); - String getId(); - String getType(); - List getArtifacts(); - List getDependencies(); - } - - /** - * Build artifact - */ - interface Artifact { - String getType(); - String getSha1(); - String getSha256(); - String getMd5(); - String getName(); - String getPath(); - String getOriginalDeploymentRepo(); - } - - /** - * Build dependency - */ - interface Dependency { - String getType(); - String getSha1(); - String getSha256(); - String getMd5(); - String getId(); - List getScopes(); - List> getRequestedBy(); - } - - /** - * Issues information - */ - interface Issues { - Tracker getTracker(); - Boolean getAggregateBuildIssues(); - String getAggregationBuildStatus(); - List getAffectedIssues(); - } - - /** - * Issue tracker information - */ - interface Tracker { - String getName(); - String getVersion(); - } - - /** - * Affected issue - */ - interface AffectedIssue { - String getKey(); - String getUrl(); - String getSummary(); - Boolean getAggregated(); - } -} - -// Made with Bob diff --git a/build.gradle b/build.gradle index a3f852f1..80db9e4d 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,7 @@ subprojects { implementation 'com.fasterxml.jackson.core:jackson-databind:2.21.1' implementation 'com.fasterxml.jackson.core:jackson-annotations:2.21' api 'org.jfrog.filespecs:file-specs-java:1.1.2' + api 'org.jfrog.buildinfo:build-info-api:2.+' } task sourcesJar(type: Jar, dependsOn: classes) { diff --git a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java index 49044029..0f9d0447 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java +++ b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java @@ -5,13 +5,13 @@ import org.jfrog.artifactory.client.Builds; import org.jfrog.artifactory.client.impl.util.Util; import org.jfrog.artifactory.client.model.AllBuilds; -import org.jfrog.artifactory.client.model.BuildInfo; import org.jfrog.artifactory.client.model.BuildPromotionRequest; import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; import org.jfrog.artifactory.client.model.impl.AllBuildsImpl; import org.jfrog.artifactory.client.model.impl.BuildPromotionResponseImpl; import org.jfrog.artifactory.client.model.impl.BuildRunsImpl; +import org.jfrog.build.api.Build; import java.io.IOException; import java.util.HashMap; @@ -40,12 +40,12 @@ public BuildRuns getBuildRuns(String buildName) throws IOException { } @Override - public void uploadBuild(BuildInfo buildInfo) throws IOException { - uploadBuild(buildInfo, null); + public void uploadBuild(Build build) throws IOException { + uploadBuild(build, null); } @Override - public void uploadBuild(BuildInfo buildInfo, String project) throws IOException { + public void uploadBuild(Build build, String project) throws IOException { String apiPath = getBuilderApi(); if (project != null && !project.isEmpty()) { apiPath += "?project=" + project; @@ -53,7 +53,7 @@ public void uploadBuild(BuildInfo buildInfo, String project) throws IOException Map headers = new HashMap<>(); artifactory.put(apiPath, ContentType.APPLICATION_JSON, - Util.getStringFromObject(buildInfo), headers, null, -1, + Util.getStringFromObject(build), headers, null, -1, String.class, null); } diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java deleted file mode 100644 index 087491c3..00000000 --- a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildInfoImpl.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.jfrog.artifactory.client.model.impl; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import org.jfrog.artifactory.client.model.BuildInfo; - -import java.util.List; -import java.util.Map; - -/** - * Implementation of BuildInfo - * - * @author rnc - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class BuildInfoImpl implements BuildInfo { - private String version; - private String name; - private String number; - private String type; - private BuildAgent buildAgent; - private Agent agent; - private String started; - private String artifactoryPluginVersion; - private Long durationMillis; - private String artifactoryPrincipal; - private String url; - private String vcsRevision; - private String vcsUrl; - private List vcs; - private LicenseControl licenseControl; - private BuildRetention buildRetention; - private List modules; - private Issues issues; - private Map governance; - private Map properties; - - @Override - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - @Override - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String getNumber() { - return number; - } - - public void setNumber(String number) { - this.number = number; - } - - @Override - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - @Override - public BuildAgent getBuildAgent() { - return buildAgent; - } - - public void setBuildAgent(BuildAgent buildAgent) { - this.buildAgent = buildAgent; - } - - @Override - public Agent getAgent() { - return agent; - } - - public void setAgent(Agent agent) { - this.agent = agent; - } - - @Override - public String getStarted() { - return started; - } - - public void setStarted(String started) { - this.started = started; - } - - @Override - public String getArtifactoryPluginVersion() { - return artifactoryPluginVersion; - } - - public void setArtifactoryPluginVersion(String artifactoryPluginVersion) { - this.artifactoryPluginVersion = artifactoryPluginVersion; - } - - @Override - public Long getDurationMillis() { - return durationMillis; - } - - public void setDurationMillis(Long durationMillis) { - this.durationMillis = durationMillis; - } - - @Override - public String getArtifactoryPrincipal() { - return artifactoryPrincipal; - } - - public void setArtifactoryPrincipal(String artifactoryPrincipal) { - this.artifactoryPrincipal = artifactoryPrincipal; - } - - @Override - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - @Override - public String getVcsRevision() { - return vcsRevision; - } - - public void setVcsRevision(String vcsRevision) { - this.vcsRevision = vcsRevision; - } - - @Override - public String getVcsUrl() { - return vcsUrl; - } - - public void setVcsUrl(String vcsUrl) { - this.vcsUrl = vcsUrl; - } - - @Override - public List getVcs() { - return vcs; - } - - public void setVcs(List vcs) { - this.vcs = vcs; - } - - @Override - public LicenseControl getLicenseControl() { - return licenseControl; - } - - public void setLicenseControl(LicenseControl licenseControl) { - this.licenseControl = licenseControl; - } - - @Override - public BuildRetention getBuildRetention() { - return buildRetention; - } - - public void setBuildRetention(BuildRetention buildRetention) { - this.buildRetention = buildRetention; - } - - @Override - public List getModules() { - return modules; - } - - public void setModules(List modules) { - this.modules = modules; - } - - @Override - public Issues getIssues() { - return issues; - } - - public void setIssues(Issues issues) { - this.issues = issues; - } - - @Override - public Map getGovernance() { - return governance; - } - - public void setGovernance(Map governance) { - this.governance = governance; - } - - @Override - public Map getProperties() { - return properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } -} - -// Made with Bob diff --git a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java index d84de0b4..4799ec48 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java +++ b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java @@ -2,13 +2,12 @@ import org.apache.commons.lang3.StringUtils; import org.jfrog.artifactory.client.model.AllBuilds; -import org.jfrog.artifactory.client.model.Build; -import org.jfrog.artifactory.client.model.BuildInfo; import org.jfrog.artifactory.client.model.BuildNumber; import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.BuildRuns; import org.jfrog.artifactory.client.model.PromotionMessage; import org.jfrog.artifactory.client.model.impl.BuildPromotionRequestImpl; +import org.jfrog.build.api.Build; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -17,8 +16,8 @@ import java.util.List; import java.util.Map; +import static org.jfrog.artifactory.client.Utils.createBuild; import static org.jfrog.artifactory.client.Utils.createBuildBody; -import static org.jfrog.artifactory.client.Utils.createBuildInfo; import static org.jfrog.artifactory.client.Utils.uploadBuild; import static org.testng.Assert.*; @@ -50,12 +49,12 @@ public void testGetAllBuilds() throws Exception { assertNotNull(allBuilds); assertTrue(StringUtils.contains(allBuilds.getUri(), BUILDS_API), allBuilds.getUri() + " is expected to contains '" + BUILDS_API + "'"); - List actualBuilds = allBuilds.getBuilds(); + List actualBuilds = allBuilds.getBuilds(); assertNotNull(actualBuilds); // Assert build uri "/TestBuild" exist String expectedBuildUri = "/" + getExpectedBuildName(); - Build actualBuild = actualBuilds.stream() + org.jfrog.artifactory.client.model.Build actualBuild = actualBuilds.stream() .filter(build -> StringUtils.equals(build.getUri(), expectedBuildUri)) .findAny().orElse(null); assertNotNull(actualBuild, "Build Uri " + expectedBuildUri + " does not exist in [" + actualBuilds + "]"); @@ -81,15 +80,15 @@ public void testGetBuildRuns() throws IOException { @Test public void testUploadBuild() throws IOException { - // Create a new build info - BuildInfo buildInfo = createBuildInfo(); + // Create a new build using the build-info API + Build build = createBuild(); // Modify the build name and number to avoid conflicts - ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setName(UPLOAD_TEST_BUILD_NAME); - ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setNumber(UPLOAD_TEST_BUILD_NUMBER); + build.setName(UPLOAD_TEST_BUILD_NAME); + build.setNumber(UPLOAD_TEST_BUILD_NUMBER); // Upload the build - artifactory.builds().uploadBuild(buildInfo); + artifactory.builds().uploadBuild(build); // Verify the build was uploaded by retrieving it BuildRuns buildRuns = artifactory.builds().getBuildRuns(UPLOAD_TEST_BUILD_NAME); @@ -104,11 +103,11 @@ public void testUploadBuild() throws IOException { @Test public void testPromoteBuild() throws IOException { - // First upload a build to promote - BuildInfo buildInfo = createBuildInfo(); - ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setName(PROMOTE_TEST_BUILD_NAME); - ((org.jfrog.artifactory.client.model.impl.BuildInfoImpl) buildInfo).setNumber(PROMOTE_TEST_BUILD_NUMBER); - artifactory.builds().uploadBuild(buildInfo); + // First upload a build to promote using the build-info API + Build build = createBuild(); + build.setName(PROMOTE_TEST_BUILD_NAME); + build.setNumber(PROMOTE_TEST_BUILD_NUMBER); + artifactory.builds().uploadBuild(build); // Create promotion request BuildPromotionRequestImpl promotionRequest = new BuildPromotionRequestImpl(); diff --git a/services/src/test/java/org/jfrog/artifactory/client/Utils.java b/services/src/test/java/org/jfrog/artifactory/client/Utils.java index 23ca5f57..b565871a 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/Utils.java +++ b/services/src/test/java/org/jfrog/artifactory/client/Utils.java @@ -5,8 +5,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.jfrog.artifactory.client.impl.ArtifactoryRequestImpl; -import org.jfrog.artifactory.client.model.BuildInfo; -import org.jfrog.artifactory.client.model.impl.BuildInfoImpl; +import org.jfrog.build.api.Build; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -43,13 +42,13 @@ public static Map createBuildBody() { return new HashMap<>(); } - public static BuildInfo createBuildInfo() { + public static Build createBuild() { String buildStarted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(System.currentTimeMillis()); try { String buildInfoJson = IOUtils.toString(Utils.class.getResourceAsStream("/build.json"), StandardCharsets.UTF_8); buildInfoJson = StringUtils.replace(buildInfoJson, "{build.start.time}", buildStarted); ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(buildInfoJson, BuildInfoImpl.class); + return mapper.readValue(buildInfoJson, Build.class); } catch (IOException e) { fail(ExceptionUtils.getRootCauseMessage(e)); } From 9cf057f63f902cf411247508659e813b3abb48e4 Mon Sep 17 00:00:00 2001 From: Nick Cross Date: Thu, 2 Jul 2026 16:58:06 +0100 Subject: [PATCH 3/4] Fix deserialization issue with promotionmessage --- .../client/model/impl/BuildPromotionResponseImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java index 36017c87..611becb6 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java +++ b/services/src/main/java/org/jfrog/artifactory/client/model/impl/BuildPromotionResponseImpl.java @@ -1,5 +1,6 @@ package org.jfrog.artifactory.client.model.impl; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.jfrog.artifactory.client.model.BuildPromotionResponse; import org.jfrog.artifactory.client.model.PromotionMessage; @@ -11,6 +12,7 @@ * @author rnc */ public class BuildPromotionResponseImpl implements BuildPromotionResponse { + @JsonDeserialize(contentAs = PromotionMessageImpl.class) private List messages; @Override From 3d57dc7ab5c7e232cc773a400010a8edc0b00bcb Mon Sep 17 00:00:00 2001 From: Nick Cross Date: Fri, 14 Aug 2026 10:52:58 +0100 Subject: [PATCH 4/4] Fix http response handling. Improve ObjectMapper. --- .../client/impl/ArtifactoryImpl.java | 57 +++++--- .../client/impl/ArtifactoryResponseImpl.java | 20 ++- .../client/impl/RepositoryHandleImpl.groovy | 9 +- .../client/aql/AqlQueryBuilder.java | 28 ++-- .../artifactory/client/impl/BuildsImpl.java | 20 ++- .../artifactory/client/impl/util/Util.java | 129 +++++++++++++----- .../jfrog/artifactory/client/BuildsTests.java | 12 ++ 7 files changed, 187 insertions(+), 88 deletions(-) diff --git a/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryImpl.java b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryImpl.java index 61cd3c40..e05e37d4 100644 --- a/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryImpl.java +++ b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryImpl.java @@ -21,8 +21,12 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * @author jbaruch @@ -31,6 +35,21 @@ */ public class ArtifactoryImpl implements Artifactory { + /** + * HTTP status codes that indicate a successful response. Shared across all verb methods to + * ensure consistent error-detection behaviour — previously post() and patch() had no check + * at all, causing Jackson to receive HTML error pages and throw JsonParseException. + */ + private static final Set SUCCESS_CODES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + HttpStatus.SC_OK, // 200 + HttpStatus.SC_CREATED, // 201 + HttpStatus.SC_ACCEPTED, // 202 + HttpStatus.SC_NO_CONTENT, // 204 + HttpStatus.SC_PARTIAL_CONTENT // 206 + )) + ); + private String username; private String url; private String userAgent; @@ -261,6 +280,19 @@ private HttpResponseException newHttpResponseException(HttpResponse httpResponse return new HttpResponseException(statusLine.getStatusCode(), artifactoryResponse); } + /** + * Throws {@link HttpResponseException} when the HTTP status code is not in + * {@link #SUCCESS_CODES}. Called by every verb method before attempting deserialisation, + * so that callers always receive a meaningful exception rather than a Jackson parse error + * caused by an HTML error page or a partial/garbage JSON body from an error response. + */ + private void assertSuccess(HttpResponse response) throws IOException { + int status = response.getStatusLine().getStatusCode(); + if (!SUCCESS_CODES.contains(status)) { + throw newHttpResponseException(response); + } + } + protected Boolean head(String path) throws IOException { HttpHead httpHead = new HttpHead(); httpHead.setURI(URI.create(url + path)); @@ -285,11 +317,7 @@ public T get(String path, Class object, Class interfaceObjec } HttpResponse httpResponse = execute(httpGet); - int status = httpResponse.getStatusLine().getStatusCode(); - if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NO_CONTENT && - status != HttpStatus.SC_ACCEPTED && status != HttpStatus.SC_PARTIAL_CONTENT) { - throw newHttpResponseException(httpResponse); - } + assertSuccess(httpResponse); if (object == null) { return (T) httpResponse; @@ -314,6 +342,7 @@ public T post(String path, org.apache.http.entity.ContentType contentType, S httpPost.setEntity(new StringEntity(content, contentType)); } HttpResponse httpResponse = execute(httpPost); + assertSuccess(httpResponse); if (object == String.class) { return (T) Util.responseToString(httpResponse); } @@ -336,6 +365,7 @@ public T patch(String path, org.apache.http.entity.ContentType contentType, httpPatch.setEntity(new StringEntity(content, contentType)); } HttpResponse httpResponse = execute(httpPatch); + assertSuccess(httpResponse); if (object == String.class) { return (T) Util.responseToString(httpResponse); } @@ -368,15 +398,11 @@ public T put(String path, org.apache.http.entity.ContentType contentType, St } } HttpResponse httpResponse = execute(httpPut); - int status = httpResponse.getStatusLine().getStatusCode(); - if (status == HttpStatus.SC_OK || status == HttpStatus.SC_NO_CONTENT || status == HttpStatus.SC_ACCEPTED || status == HttpStatus.SC_CREATED) { - if (object == String.class) { - return (T) Util.responseToString(httpResponse); - } - return Util.responseToObject(httpResponse, object, interfaceObject); + assertSuccess(httpResponse); + if (object == String.class) { + return (T) Util.responseToString(httpResponse); } - - throw newHttpResponseException(httpResponse); + return Util.responseToObject(httpResponse, object, interfaceObject); } public String delete(String path) throws IOException { @@ -384,10 +410,7 @@ public String delete(String path) throws IOException { httpDelete.setURI(URI.create(url + path)); HttpResponse httpResponse = execute(httpDelete); - int status = httpResponse.getStatusLine().getStatusCode(); - if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NO_CONTENT && status != HttpStatus.SC_ACCEPTED) { - throw newHttpResponseException(httpResponse); - } + assertSuccess(httpResponse); return Util.responseToString(httpResponse); } diff --git a/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryResponseImpl.java b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryResponseImpl.java index 9ca8328d..cd68d96d 100644 --- a/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryResponseImpl.java +++ b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryResponseImpl.java @@ -1,6 +1,5 @@ package org.jfrog.artifactory.client.impl; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.util.EntityUtils; @@ -11,8 +10,6 @@ public class ArtifactoryResponseImpl extends AbstractArtifactoryResponseImpl implements ArtifactoryResponse { - private static final ObjectMapper objectMapper = new ObjectMapper(); - private String rawBody; ArtifactoryResponseImpl(HttpResponse httpResponse) throws IOException { @@ -36,13 +33,24 @@ public String getRawBody() { return this.rawBody; } + /** + * Deserialise the response body to {@code toType} using the shared + * {@link Util#CONFIGURED_MAPPER}. + * + *

Previously this method called {@code Util.configureObjectMapper(objectMapper)} on + * every invocation, which mutated the static field's mapper state under concurrent use. + * Delegating to the already-configured singleton removes both the mutation hazard and the + * unnecessary per-call configuration overhead. + */ @Override public T parseBody(Class toType) throws IOException { - Util.configureObjectMapper(objectMapper); try { - return objectMapper.readValue(rawBody, toType); + return Util.CONFIGURED_MAPPER.readValue(rawBody, toType); } catch (IOException e) { - throw new IOException("Failed casting response entity to " + toType.toString() + ". response status: " + getStatusLine().toString() + ". raw entity: " + this.rawBody, e); + throw new IOException( + "Failed casting response entity to " + toType + + ". response status: " + getStatusLine() + + ". raw entity: " + this.rawBody, e); } } diff --git a/services/src/main/groovy/org/jfrog/artifactory/client/impl/RepositoryHandleImpl.groovy b/services/src/main/groovy/org/jfrog/artifactory/client/impl/RepositoryHandleImpl.groovy index 4a0121c8..1051738f 100644 --- a/services/src/main/groovy/org/jfrog/artifactory/client/impl/RepositoryHandleImpl.groovy +++ b/services/src/main/groovy/org/jfrog/artifactory/client/impl/RepositoryHandleImpl.groovy @@ -1,7 +1,6 @@ package org.jfrog.artifactory.client.impl import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper import org.jfrog.artifactory.client.* import org.jfrog.artifactory.client.model.ItemPermission import org.jfrog.artifactory.client.impl.util.Util @@ -18,8 +17,6 @@ import java.beans.BeanInfo import java.beans.Introspector import java.beans.PropertyDescriptor -import static org.jfrog.artifactory.client.impl.util.Util.configureObjectMapper - /** * * @author jbaruch @@ -135,11 +132,7 @@ class RepositoryHandleImpl implements RepositoryHandle { @Override boolean isFolder(String path) { String itemInfoJson = artifactory.get("/api/storage/${repoKey}/${path}", String, null) - - ObjectMapper objectMapper = new ObjectMapper() - configureObjectMapper(objectMapper) - JsonNode jsonNode = objectMapper.readTree(itemInfoJson) - + JsonNode jsonNode = Util.CONFIGURED_MAPPER.readTree(itemInfoJson) return jsonNode.get("children") != null } diff --git a/services/src/main/java/org/jfrog/artifactory/client/aql/AqlQueryBuilder.java b/services/src/main/java/org/jfrog/artifactory/client/aql/AqlQueryBuilder.java index f549b97a..73e5d61b 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/aql/AqlQueryBuilder.java +++ b/services/src/main/java/org/jfrog/artifactory/client/aql/AqlQueryBuilder.java @@ -109,34 +109,40 @@ public AqlQueryBuilder offset(int offset) { return this; } + /** + * Plain mapper for AQL structures — no mix-ins or abstract-type resolvers are needed here + * because AQL only deals with plain Map/Integer/String values. Must not share + * {@code Util.CONFIGURED_MAPPER} which carries Repository mix-ins inappropriate for this use. + */ + private static final ObjectMapper PLAIN_MAPPER = new ObjectMapper(); + public String build() { try { - ObjectMapper mapper = new ObjectMapper(); - return "items.find(" + getRootAsString(mapper) + ")" + getIncludeAsString() + getSortAsString( - mapper) + getOffsetAsString(mapper) + getLimitAsString(mapper); + return "items.find(" + getRootAsString() + ")" + getIncludeAsString() + + getSortAsString() + getOffsetAsString() + getLimitAsString(); } catch (JsonProcessingException e) { throw new AqlBuilderException("Error serializing object to json: ", e); } } - private String getSortAsString(ObjectMapper mapper) throws JsonProcessingException { - return hasSort() ? ".sort(" + mapper.writeValueAsString(sort) + ")" : ""; + private String getSortAsString() throws JsonProcessingException { + return hasSort() ? ".sort(" + PLAIN_MAPPER.writeValueAsString(sort) + ")" : ""; } private String getIncludeAsString() { return hasInclude() ? include.toString() : ""; } - private String getOffsetAsString(ObjectMapper mapper) throws JsonProcessingException { - return hasOffset() ? ".offset(" + mapper.writeValueAsString(offset) + ")" : ""; + private String getOffsetAsString() throws JsonProcessingException { + return hasOffset() ? ".offset(" + PLAIN_MAPPER.writeValueAsString(offset) + ")" : ""; } - private String getLimitAsString(ObjectMapper mapper) throws JsonProcessingException { - return hasLimit() ? ".limit(" + mapper.writeValueAsString(limit) + ")" : ""; + private String getLimitAsString() throws JsonProcessingException { + return hasLimit() ? ".limit(" + PLAIN_MAPPER.writeValueAsString(limit) + ")" : ""; } - private String getRootAsString(ObjectMapper mapper) throws JsonProcessingException { - return hasRoot() ? mapper.writeValueAsString(root) : ""; + private String getRootAsString() throws JsonProcessingException { + return hasRoot() ? PLAIN_MAPPER.writeValueAsString(root) : ""; } private boolean hasInclude() { diff --git a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java index 0f9d0447..da940e87 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java +++ b/services/src/main/java/org/jfrog/artifactory/client/impl/BuildsImpl.java @@ -14,8 +14,6 @@ import org.jfrog.build.api.Build; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; /** * @author yahavi @@ -48,30 +46,28 @@ public void uploadBuild(Build build) throws IOException { public void uploadBuild(Build build, String project) throws IOException { String apiPath = getBuilderApi(); if (project != null && !project.isEmpty()) { - apiPath += "?project=" + project; + apiPath += "?project=" + Util.encodeParams(project); } - - Map headers = new HashMap<>(); artifactory.put(apiPath, ContentType.APPLICATION_JSON, - Util.getStringFromObject(build), headers, null, -1, + Util.getStringFromObject(build), null, null, -1, String.class, null); } @Override - public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest) throws IOException { + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, + BuildPromotionRequest promotionRequest) throws IOException { return promoteBuild(buildName, buildNumber, promotionRequest, null); } @Override - public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, BuildPromotionRequest promotionRequest, String project) throws IOException { + public BuildPromotionResponse promoteBuild(String buildName, String buildNumber, + BuildPromotionRequest promotionRequest, String project) throws IOException { String apiPath = getBuilderApi() + "promote/" + buildName + "/" + buildNumber; if (project != null && !project.isEmpty()) { - apiPath += "?project=" + project; + apiPath += "?project=" + Util.encodeParams(project); } - - Map headers = new HashMap<>(); return artifactory.post(apiPath, ContentType.APPLICATION_JSON, - Util.getStringFromObject(promotionRequest), headers, + Util.getStringFromObject(promotionRequest), null, BuildPromotionResponseImpl.class, BuildPromotionResponse.class); } diff --git a/services/src/main/java/org/jfrog/artifactory/client/impl/util/Util.java b/services/src/main/java/org/jfrog/artifactory/client/impl/util/Util.java index 5a2049c2..a9e7b83a 100644 --- a/services/src/main/java/org/jfrog/artifactory/client/impl/util/Util.java +++ b/services/src/main/java/org/jfrog/artifactory/client/impl/util/Util.java @@ -29,8 +29,14 @@ import org.jfrog.artifactory.client.ArtifactoryRequest; import org.jfrog.artifactory.client.impl.jackson.RepositoryMixIn; import org.jfrog.artifactory.client.impl.jackson.RepositorySettingsMixIn; +import org.jfrog.artifactory.client.model.AllBuilds; +import org.jfrog.artifactory.client.model.BuildPromotionResponse; +import org.jfrog.artifactory.client.model.BuildRuns; import org.jfrog.artifactory.client.model.PackageType; import org.jfrog.artifactory.client.model.Repository; +import org.jfrog.artifactory.client.model.impl.AllBuildsImpl; +import org.jfrog.artifactory.client.model.impl.BuildPromotionResponseImpl; +import org.jfrog.artifactory.client.model.impl.BuildRunsImpl; import org.jfrog.artifactory.client.model.repository.settings.RepositorySettings; /** @@ -38,22 +44,82 @@ */ public class Util { - public static T responseToObject(HttpResponse httpResponse, Class object, Class interfaceClass) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - configureObjectMapper(objectMapper); - - if (interfaceClass != null) { - SimpleModule module = new SimpleModule("CustomModel", Version.unknownVersion()); - SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver(); - resolver.addMapping(interfaceClass, object); - module.setAbstractTypes(resolver); - objectMapper.registerModule(module); - } + /** + * Shared, fully-configured, thread-safe {@link ObjectMapper}. + * + *

Jackson's ObjectMapper is heavyweight at construction time (module scanning, + * type-factory setup, annotation-introspector initialisation) and thread-safe after + * configuration. All production code in this library must use this instance rather than + * creating a new one per call. + * + *

Configuration applied once at class-load time: + *

    + *
  • Mix-ins for {@code Repository} and {@code RepositorySettings} (required for + * correct polymorphic serialisation of repository types)
  • + *
  • Abstract-type mappings for all known API interface→impl pairs (replaces the + * per-call {@link SimpleModule} that was previously registered inside + * {@link #responseToObject})
  • + *
  • Standard feature flags ({@code FAIL_ON_UNKNOWN_PROPERTIES=false}, etc.)
  • + *
  • {@code INDENT_OUTPUT=true} (matches previous per-call behaviour of + * {@link #getStringFromObject})
  • + *
+ */ + public static final ObjectMapper CONFIGURED_MAPPER = createConfiguredMapper(); + + private static ObjectMapper createConfiguredMapper() { + ObjectMapper om = new ObjectMapper(); + + // Mix-ins + om.addMixIn(Repository.class, RepositoryMixIn.class); + om.addMixIn(RepositorySettings.class, RepositorySettingsMixIn.class); + + // Feature flags + om.configure(WRITE_DATES_AS_TIMESTAMPS, false); + om.setVisibility(defaultInstance().withFieldVisibility(JsonAutoDetect.Visibility.ANY)); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + om.configure(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS, false); + om.setSerializationInclusion(JsonInclude.Include.NON_NULL); + om.configure(SerializationFeature.INDENT_OUTPUT, true); + + // Pre-register all known interface→impl abstract-type mappings. + // Previously a new SimpleModule was created and registered on every call to + // responseToObject(); pre-registering them here removes that per-call overhead. + SimpleModule module = new SimpleModule("ClientModel", Version.unknownVersion()); + SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver(); + resolver.addMapping(AllBuilds.class, AllBuildsImpl.class); + resolver.addMapping(BuildRuns.class, BuildRunsImpl.class); + resolver.addMapping(BuildPromotionResponse.class, BuildPromotionResponseImpl.class); + module.setAbstractTypes(resolver); + om.registerModule(module); + + return om; + } + /** + * Deserialise an HTTP response body into an instance of {@code object}. + * + *

The {@code interfaceClass} parameter is retained for binary compatibility but is no + * longer used — all known interface→impl mappings are pre-registered on + * {@link #CONFIGURED_MAPPER}. + * + * @param httpResponse the raw HTTP response whose entity will be read + * @param object the concrete class to deserialise into + * @param interfaceClass unused; kept for binary compatibility + */ + public static T responseToObject(HttpResponse httpResponse, Class object, + Class interfaceClass) throws IOException { String content = EntityUtils.toString(httpResponse.getEntity(), "UTF-8"); - return objectMapper.readValue(content, object); + return CONFIGURED_MAPPER.readValue(content, object); } + /** + * Configure an external {@link ObjectMapper} with the same settings as + * {@link #CONFIGURED_MAPPER}. + * + * @deprecated Use {@link #CONFIGURED_MAPPER} directly. This method mutates a shared mapper + * on every call and will be removed in a future major version. + */ + @Deprecated public static void configureObjectMapper(ObjectMapper objectMapper) { objectMapper.addMixIn(Repository.class, RepositoryMixIn.class); objectMapper.addMixIn(RepositorySettings.class, RepositorySettingsMixIn.class); @@ -66,26 +132,25 @@ public static void configureObjectMapper(ObjectMapper objectMapper) { public static String responseToString(HttpResponse httpResponse) throws IOException { if (httpResponse.getEntity() != null) { - try(InputStream in = httpResponse.getEntity().getContent();) { + try (InputStream in = httpResponse.getEntity().getContent()) { return IOUtils.toString(in, "UTF-8"); } } return null; } + /** + * Serialise {@code object} to a pretty-printed JSON string. + */ public static String getStringFromObject(Object object) throws JsonProcessingException { if (object == null) { return null; } - ObjectMapper objectMapper = new ObjectMapper(); - configureObjectMapper(objectMapper); - objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); - - return objectMapper.writeValueAsString(object); + return CONFIGURED_MAPPER.writeValueAsString(object); } - public static ContentType getContentType(ArtifactoryRequest.ContentType contentType) { - switch(contentType){ + public static ContentType getContentType(ArtifactoryRequest.ContentType contentType) { + switch (contentType) { case JSON: return ContentType.APPLICATION_JSON; case JOSE: @@ -108,22 +173,20 @@ public static ContentType getContentType(ArtifactoryRequest.ContentType contentT } public static T parseText(String text, Class target) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - configureObjectMapper(objectMapper); - return objectMapper.readValue(text, target); + return CONFIGURED_MAPPER.readValue(text, target); } - public static T parseObjectWithTypeReference(String content, TypeReference typeReference) throws IOException { - ObjectMapper objectMapper = new ObjectMapper(); - configureObjectMapper(objectMapper); - return objectMapper.readValue(content, typeReference); + public static T parseObjectWithTypeReference(String content, + TypeReference typeReference) throws IOException { + return CONFIGURED_MAPPER.readValue(content, typeReference); } public static String encodeParams(String param) throws UnsupportedEncodingException { return URLEncoder.encode(param, "UTF-8"); } - public static String getQueryPath(String startingParam, Map paramsMap) throws UnsupportedEncodingException { + public static String getQueryPath(String startingParam, + Map paramsMap) throws UnsupportedEncodingException { StringBuilder queryPath = new StringBuilder(startingParam); Iterator> it = paramsMap.entrySet().iterator(); while (it.hasNext()) { @@ -131,23 +194,21 @@ public static String getQueryPath(String startingParam, Map para String key = pair.getKey(); String value = pair.getValue(); queryPath.append(encodeParams(key)).append("=").append(Util.encodeParams(value)); - if(it.hasNext()){ + if (it.hasNext()) { queryPath.append("&"); } } return queryPath.toString(); } - public static Class getRepositorySettingsClassForPackageType(PackageType packageType) { + public static Class getRepositorySettingsClassForPackageType( + PackageType packageType) { JsonSubTypes annotation = RepositorySettingsMixIn.class.getDeclaredAnnotation(JsonSubTypes.class); - for (JsonSubTypes.Type type : annotation.value()) { if (type.name().equals(packageType.name())) { - return (Class)type.value(); + return (Class) type.value(); } } - return null; } - } diff --git a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java index 4799ec48..3031aba6 100644 --- a/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java +++ b/services/src/test/java/org/jfrog/artifactory/client/BuildsTests.java @@ -1,6 +1,7 @@ package org.jfrog.artifactory.client; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.HttpResponseException; import org.jfrog.artifactory.client.model.AllBuilds; import org.jfrog.artifactory.client.model.BuildNumber; import org.jfrog.artifactory.client.model.BuildPromotionResponse; @@ -141,6 +142,17 @@ public void testPromoteBuild() throws IOException { } } + @Test(expectedExceptions = HttpResponseException.class, + description = "A non-existent build must produce HttpResponseException(404), " + + "not a JsonParseException from Jackson parsing an HTML/JSON error page. " + + "Regression test for the missing status-code check in ArtifactoryImpl.post().") + public void testPromoteBuild_nonexistentBuild_throwsHttpResponseException() throws IOException { + BuildPromotionRequestImpl req = new BuildPromotionRequestImpl(); + req.setTargetRepo("any-repo"); + // This must throw HttpResponseException, not JsonParseException + artifactory.builds().promoteBuild("no-such-build-xyzzy-regression", "99999", req); + } + private String getExpectedBuildName() { return (String) buildBody.get("name"); }