Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions api/src/main/java/org/jfrog/artifactory/client/Builds.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.jfrog.artifactory.client;

import org.jfrog.artifactory.client.model.AllBuilds;
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;

Expand All @@ -12,4 +15,44 @@ public interface Builds {
AllBuilds getAllBuilds() throws IOException;

BuildRuns getBuildRuns(String buildName) throws IOException;

/**
* Upload a build to Artifactory using the official build-info API
*
* @param build the build info from org.jfrog.build.api.Build
* @throws IOException if the upload fails
*/
void uploadBuild(Build build) throws IOException;

/**
* Upload a build to Artifactory with a project parameter using the official build-info API
*
* @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(Build build, 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;
}
Original file line number Diff line number Diff line change
@@ -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<String> getScopes();

/**
* A list of properties to attach to the build's artifacts
* @return the properties
*/
Map<String, Object> 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
Original file line number Diff line number Diff line change
@@ -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<PromotionMessage> getMessages();
}

// Made with Bob
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Integer> 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;
Expand Down Expand Up @@ -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));
Expand All @@ -285,11 +317,7 @@ public <T> T get(String path, Class<? extends T> object, Class<T> 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;
Expand All @@ -314,6 +342,7 @@ public <T> 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);
}
Expand All @@ -336,6 +365,7 @@ public <T> 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);
}
Expand Down Expand Up @@ -368,26 +398,19 @@ public <T> 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 {
HttpDelete httpDelete = new HttpDelete();

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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -36,13 +33,24 @@ public String getRawBody() {
return this.rawBody;
}

/**
* Deserialise the response body to {@code toType} using the shared
* {@link Util#CONFIGURED_MAPPER}.
*
* <p>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> T parseBody(Class<T> 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);
}
}

Expand Down
Loading
Loading