From ad4ebdbadf7dff76cb64df17f21ce8c6bb8697ff Mon Sep 17 00:00:00 2001 From: adig Date: Mon, 11 Sep 2023 21:20:11 +0300 Subject: [PATCH] Add Streaming Rest Call - returning the response input stream without the wrappers --- README.md | 25 +++++++- .../jfrog/artifactory/client/Artifactory.java | 6 ++ .../client/ArtifactoryResponse.java | 11 +--- .../client/ArtifactoryStreamingResponse.java | 13 +++++ .../client/BaseArtifactoryResponse.java | 13 +++++ .../impl/AbstractArtifactoryResponseImpl.java | 27 +++++++++ .../client/impl/ArtifactoryImpl.java | 21 ++++++- .../client/impl/ArtifactoryResponseImpl.java | 18 +----- .../ArtifactoryStreamingResponseImpl.java | 39 +++++++++++++ .../client/StreamingRestCallTest.java | 57 +++++++++++++++++++ 10 files changed, 202 insertions(+), 28 deletions(-) create mode 100644 api/src/main/java/org/jfrog/artifactory/client/ArtifactoryStreamingResponse.java create mode 100644 api/src/main/java/org/jfrog/artifactory/client/BaseArtifactoryResponse.java create mode 100644 services/src/main/groovy/org/jfrog/artifactory/client/impl/AbstractArtifactoryResponseImpl.java create mode 100644 services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryStreamingResponseImpl.java create mode 100644 services/src/test/java/org/jfrog/artifactory/client/StreamingRestCallTest.java diff --git a/README.md b/README.md index e7517e7c..ac178b96 100644 --- a/README.md +++ b/README.md @@ -851,16 +851,37 @@ org.apache.http.Header[] headers = response.getAllHeaders(); org.apache.http.StatusLine statusLine = response.getStatusLine(); // A convenience method for verifying success -assert response.isSuccessResponse() +assert response.isSuccessResponse(); // Get the response raw body -String rawBody = response.rawBody(); +String rawBody = response.getRawBody(); // If the the response raw body has a JSON format, populate an object with the body content, // by providing a object's class. List> parsedBody = response.parseBody(List.class); ``` +Executing an Artifactory streaming REST API + +```groovy +ArtifactoryRequest repositoryRequest = new ArtifactoryRequestImpl().apiUrl("api/repositories") + .method(ArtifactoryRequest.Method.GET) + .responseType(ArtifactoryRequest.ContentType.JSON); +ArtifactoryStreamingResponse response = artifactory.streamingRestCall(repositoryRequest); + +// Get the response headers +org.apache.http.Header[] headers = response.getAllHeaders(); + +// Get the response status information +org.apache.http.StatusLine statusLine = response.getStatusLine(); + +// A convenience method for verifying success +assert response.isSuccessResponse(); + +// Get the response raw body using input stream +String rawBody = IOUtils.toString(response.getInputStream(), StandardCharsets.UTF_8); +``` + ## Building and Testing the Sources The code is built using Gradle and includes integration tests. diff --git a/api/src/main/java/org/jfrog/artifactory/client/Artifactory.java b/api/src/main/java/org/jfrog/artifactory/client/Artifactory.java index 602ac661..d36fbe90 100644 --- a/api/src/main/java/org/jfrog/artifactory/client/Artifactory.java +++ b/api/src/main/java/org/jfrog/artifactory/client/Artifactory.java @@ -1,6 +1,8 @@ package org.jfrog.artifactory.client; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; import java.io.IOException; import java.io.InputStream; @@ -42,10 +44,14 @@ public interface Artifactory extends ApiInterface, AutoCloseable { ArtifactoryResponse restCall(ArtifactoryRequest artifactoryRequest) throws IOException; + ArtifactoryStreamingResponse streamingRestCall(ArtifactoryRequest artifactoryRequest) throws IOException; + InputStream getInputStream(String path) throws IOException; InputStream getInputStreamWithHeaders(String path, Map headers) throws IOException; + HttpResponse execute(HttpUriRequest request) throws IOException; + default public T get(String path, Class object, Class interfaceObject) throws IOException { return null; } diff --git a/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryResponse.java b/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryResponse.java index 883eaa98..be1ee510 100644 --- a/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryResponse.java +++ b/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryResponse.java @@ -1,7 +1,5 @@ package org.jfrog.artifactory.client; -import org.apache.http.Header; -import org.apache.http.StatusLine; import java.io.IOException; @@ -9,15 +7,10 @@ * ArtifactoryResponse object returned from {@link Artifactory#restCall(ArtifactoryRequest)}. * acts as a wrapper for {@link org.apache.http.HttpResponse} but removes the need to handle response stream. */ -public interface ArtifactoryResponse { - - Header[] getAllHeaders(); - - StatusLine getStatusLine(); +public interface ArtifactoryResponse extends BaseArtifactoryResponse { String getRawBody(); T parseBody(Class toType) throws IOException; - boolean isSuccessResponse(); -} +} \ No newline at end of file diff --git a/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryStreamingResponse.java b/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryStreamingResponse.java new file mode 100644 index 00000000..0cb2deb0 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/ArtifactoryStreamingResponse.java @@ -0,0 +1,13 @@ +package org.jfrog.artifactory.client; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * ArtifactoryStreamingResponse object returned from {@link Artifactory#streamingRestCall(ArtifactoryRequest)}. + * acts as a wrapper for {@link org.apache.http.HttpResponse}. + */ +public interface ArtifactoryStreamingResponse extends BaseArtifactoryResponse, AutoCloseable { + InputStream getInputStream() throws IOException; +} diff --git a/api/src/main/java/org/jfrog/artifactory/client/BaseArtifactoryResponse.java b/api/src/main/java/org/jfrog/artifactory/client/BaseArtifactoryResponse.java new file mode 100644 index 00000000..d52c2258 --- /dev/null +++ b/api/src/main/java/org/jfrog/artifactory/client/BaseArtifactoryResponse.java @@ -0,0 +1,13 @@ +package org.jfrog.artifactory.client; +import org.apache.http.Header; +import org.apache.http.StatusLine; + +public interface BaseArtifactoryResponse { + + Header[] getAllHeaders(); + + StatusLine getStatusLine(); + + boolean isSuccessResponse(); + +} diff --git a/services/src/main/groovy/org/jfrog/artifactory/client/impl/AbstractArtifactoryResponseImpl.java b/services/src/main/groovy/org/jfrog/artifactory/client/impl/AbstractArtifactoryResponseImpl.java new file mode 100644 index 00000000..cf64ccd8 --- /dev/null +++ b/services/src/main/groovy/org/jfrog/artifactory/client/impl/AbstractArtifactoryResponseImpl.java @@ -0,0 +1,27 @@ +package org.jfrog.artifactory.client.impl; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; + +public abstract class AbstractArtifactoryResponseImpl { + + private final HttpResponse httpResponse; + + public AbstractArtifactoryResponseImpl(HttpResponse httpResponse) { + this.httpResponse = httpResponse; + } + + public HttpResponse getHttpResponse() { + return httpResponse; + } + + public Header[] getAllHeaders() { + return this.httpResponse.getAllHeaders(); + } + + public StatusLine getStatusLine() { + return this.httpResponse.getStatusLine(); + } + +} 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 3093ad42..ae159a2c 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 @@ -132,6 +132,24 @@ public ArtifactorySystem system() { */ @Override public ArtifactoryResponse restCall(ArtifactoryRequest artifactoryRequest) throws IOException { + HttpResponse httpResponse = handleArtifactoryRequest(artifactoryRequest); + return new ArtifactoryResponseImpl(httpResponse); + } + + /** + * Create a REST call to artifactory with a generic request + * + * @param artifactoryRequest that should be sent to artifactory + * @return {@link ArtifactoryStreamingResponse} Artifactory response in accordance with the request, + * which includes a reference to the inputStream. + */ + @Override + public ArtifactoryStreamingResponse streamingRestCall(ArtifactoryRequest artifactoryRequest) throws IOException { + HttpResponse httpResponse = handleArtifactoryRequest(artifactoryRequest); + return new ArtifactoryStreamingResponseImpl(httpResponse); + } + + private HttpResponse handleArtifactoryRequest(ArtifactoryRequest artifactoryRequest) throws IOException { HttpRequestBase httpRequest; String requestPath = "/" + artifactoryRequest.getApiUrl(); @@ -194,7 +212,7 @@ public ArtifactoryResponse restCall(ArtifactoryRequest artifactoryRequest) throw } HttpResponse httpResponse = execute(httpRequest); - return new ArtifactoryResponseImpl(httpResponse); + return httpResponse; } private void setEntity(HttpEntityEnclosingRequestBase httpRequest, Object body, ContentType contentType) throws JsonProcessingException { @@ -369,6 +387,7 @@ public String delete(String path) throws IOException { return Util.responseToString(httpResponse); } + @Override public HttpResponse execute(HttpUriRequest request) throws IOException { HttpClientContext clientContext = HttpClientContext.create(); if (clientContext.getAttribute(PreemptiveAuthInterceptor.ORIGINAL_HOST_CONTEXT_PARAM) == null) { 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 aeefc202..9ca8328d 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,25 +1,22 @@ package org.jfrog.artifactory.client.impl; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; import org.apache.http.util.EntityUtils; import org.jfrog.artifactory.client.ArtifactoryResponse; import org.jfrog.artifactory.client.impl.util.Util; import java.io.IOException; -public class ArtifactoryResponseImpl implements ArtifactoryResponse { +public class ArtifactoryResponseImpl extends AbstractArtifactoryResponseImpl implements ArtifactoryResponse { private static final ObjectMapper objectMapper = new ObjectMapper(); - private HttpResponse httpResponse; private String rawBody; ArtifactoryResponseImpl(HttpResponse httpResponse) throws IOException { - this.httpResponse = httpResponse; + super(httpResponse); HttpEntity entity = httpResponse.getEntity(); @@ -34,16 +31,6 @@ public class ArtifactoryResponseImpl implements ArtifactoryResponse { } } - @Override - public Header[] getAllHeaders() { - return this.httpResponse.getAllHeaders(); - } - - @Override - public StatusLine getStatusLine() { - return this.httpResponse.getStatusLine(); - } - @Override public String getRawBody() { return this.rawBody; @@ -62,7 +49,6 @@ public T parseBody(Class toType) throws IOException { @Override public boolean isSuccessResponse() { int status = getStatusLine().getStatusCode(); - return status >= 200 && status < 300; } } diff --git a/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryStreamingResponseImpl.java b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryStreamingResponseImpl.java new file mode 100644 index 00000000..57080a64 --- /dev/null +++ b/services/src/main/groovy/org/jfrog/artifactory/client/impl/ArtifactoryStreamingResponseImpl.java @@ -0,0 +1,39 @@ +package org.jfrog.artifactory.client.impl; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.jfrog.artifactory.client.ArtifactoryStreamingResponse; + +import java.io.IOException; +import java.io.InputStream; + +public class ArtifactoryStreamingResponseImpl extends AbstractArtifactoryResponseImpl implements ArtifactoryStreamingResponse { + + public ArtifactoryStreamingResponseImpl(HttpResponse httpResponse) { + super(httpResponse); + } + + @Override + public InputStream getInputStream() throws IOException { + InputStream is = null; + HttpEntity entity = getHttpResponse().getEntity(); + if (entity != null) { + is = entity.getContent(); + } + return is; + } + + @Override + public boolean isSuccessResponse() { + int status = getStatusLine().getStatusCode(); + return (status == HttpStatus.SC_OK || + status == HttpStatus.SC_PARTIAL_CONTENT); + } + + @Override + public void close() throws Exception { + IOUtils.close(getInputStream()); + } +} diff --git a/services/src/test/java/org/jfrog/artifactory/client/StreamingRestCallTest.java b/services/src/test/java/org/jfrog/artifactory/client/StreamingRestCallTest.java new file mode 100644 index 00000000..c29df72a --- /dev/null +++ b/services/src/test/java/org/jfrog/artifactory/client/StreamingRestCallTest.java @@ -0,0 +1,57 @@ +package org.jfrog.artifactory.client; + +import org.apache.commons.io.IOUtils; +import org.jfrog.artifactory.client.impl.ArtifactoryRequestImpl; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.*; + +public class StreamingRestCallTest extends ArtifactoryTestsBase { + + @Test + public void testDownloadWithHeadersByStreamingRestCall() throws IOException { + InputStream inputStream = this.getClass().getResourceAsStream("/sample.txt"); + assertNotNull(inputStream); + artifactory.repository(localRepository.getKey()).upload(PATH, inputStream).withProperty("color", "blue") + .withProperty("color", "red").doUpload(); + + Map headers = new HashMap<>(); + headers.put("Range", "bytes=0-10"); + ArtifactoryRequest request = new ArtifactoryRequestImpl() + .apiUrl(localRepository.getKey() + "/" + PATH) + .method(ArtifactoryRequest.Method.GET) + .setHeaders(headers) + .requestType(ArtifactoryRequest.ContentType.JSON); + + ArtifactoryStreamingResponse response = artifactory.streamingRestCall(request); + assertTrue(response.isSuccessResponse()); + + inputStream = response.getInputStream(); + String actual = textFrom(inputStream); + assertEquals(actual, textFrom(this.getClass().getResourceAsStream("/sample.txt")).substring(0, 11)); + } + + @Test + public void testErrorStreamingRestCall() throws IOException { + ArtifactoryRequest request = new ArtifactoryRequestImpl() + .apiUrl(localRepository.getKey() + "/" + PATH + "shouldNotExist") + .method(ArtifactoryRequest.Method.GET) + .requestType(ArtifactoryRequest.ContentType.JSON); + ArtifactoryStreamingResponse response = artifactory.streamingRestCall(request); + assertFalse(response.isSuccessResponse()); + assertEquals(response.getStatusLine().getStatusCode(), 404); + String raw = IOUtils.toString(response.getInputStream(), StandardCharsets.UTF_8); + assertEquals(raw, "{\n" + + " \"errors\" : [ {\n" + + " \"status\" : 404,\n" + + " \"message\" : \"File not found.\"\n" + + " } ]\n" + + "}"); + } +}