Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate HTTP client to okhttp. #188

Merged
merged 5 commits into from Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -36,7 +36,7 @@ sourceSets {
dependencies {
compile gradleApi()
compile localGroovy()
compile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
compile 'com.squareup.okhttp3:okhttp:4.9.1'

testCompile('org.spockframework:spock-core:1.3-groovy-2.5') {
//groovy 2.3.x or 2.4.x is already provided by Gradle itself
Expand Down
Expand Up @@ -2,11 +2,10 @@ package io.codearte.gradle.nexus.functional

import com.github.tomakehurst.wiremock.junit.WireMockRule
import groovy.transform.NotYetImplemented
import groovyx.net.http.HttpResponseException
import groovyx.net.http.RESTClient
import io.codearte.gradle.nexus.infra.NexusHttpResponseException
import io.codearte.gradle.nexus.infra.SimplifiedHttpJsonRestClient
import io.codearte.gradle.nexus.logic.RepositoryCloser
import okhttp3.OkHttpClient
import org.junit.Rule
import spock.lang.Specification

Expand Down Expand Up @@ -40,7 +39,7 @@ class MockedResponseErrorHandlingSpec extends Specification {

def "should present response body on 500 server error"() {
given:
SimplifiedHttpJsonRestClient client = new SimplifiedHttpJsonRestClient(new RESTClient(), TEST_MOCKED_USERNAME, TEST_MOCKED_PASSWORD)
SimplifiedHttpJsonRestClient client = new SimplifiedHttpJsonRestClient(new OkHttpClient(), TEST_MOCKED_USERNAME, TEST_MOCKED_PASSWORD)
RepositoryCloser closer = new RepositoryCloser(client, getMockedUrl(), TEST_MOCKED_REPOSITORY_DESCRIPTION)
and:
stubFor(post(urlEqualTo("/staging/bulk/close"))
Expand All @@ -56,7 +55,6 @@ class MockedResponseErrorHandlingSpec extends Specification {
NexusHttpResponseException e = thrown()
e.statusCode == 500
e.message.contains("Missing staging repository: $TEST_MOCKED_NOT_EXISTING_REPOSITORY_ID")
e.cause instanceof HttpResponseException
}

private String getMockedUrl() {
Expand Down
@@ -1,7 +1,6 @@
package io.codearte.gradle.nexus.functional.e2e

import groovy.transform.NotYetImplemented
import groovyx.net.http.RESTClient
import io.codearte.gradle.nexus.functional.BaseNexusStagingFunctionalSpec
import io.codearte.gradle.nexus.infra.SimplifiedHttpJsonRestClient
import io.codearte.gradle.nexus.logic.OperationRetrier
Expand All @@ -15,6 +14,7 @@ import io.codearte.gradle.nexus.logic.RepositoryStateFetcher
import io.codearte.gradle.nexus.logic.RetryingRepositoryTransitioner
import io.codearte.gradle.nexus.logic.StagingProfileFetcher
import nebula.test.functional.ExecutionResult
import okhttp3.OkHttpClient
import spock.lang.Ignore
import spock.lang.Stepwise

Expand All @@ -29,7 +29,7 @@ class ExploratoryE2ESpec extends BaseNexusStagingFunctionalSpec implements E2ESp
private static String resolvedStagingRepositoryId

def setup() {
client = new SimplifiedHttpJsonRestClient(new RESTClient(), nexusUsernameAT, nexusPasswordAT)
client = new SimplifiedHttpJsonRestClient(new OkHttpClient(), nexusUsernameAT, nexusPasswordAT)
repoStateFetcher = new RepositoryStateFetcher(client, E2E_SERVER_BASE_PATH)
retrier = new OperationRetrier<>()
}
Expand Down
11 changes: 8 additions & 3 deletions src/main/groovy/io/codearte/gradle/nexus/BaseStagingTask.groovy
Expand Up @@ -2,7 +2,6 @@ package io.codearte.gradle.nexus

import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import groovyx.net.http.RESTClient
import io.codearte.gradle.nexus.infra.SimplifiedHttpJsonRestClient
import io.codearte.gradle.nexus.logic.OperationRetrier
import io.codearte.gradle.nexus.logic.RepositoryCloser
Expand All @@ -11,6 +10,7 @@ import io.codearte.gradle.nexus.logic.RepositoryReleaser
import io.codearte.gradle.nexus.logic.RepositoryState
import io.codearte.gradle.nexus.logic.RepositoryStateFetcher
import io.codearte.gradle.nexus.logic.StagingProfileFetcher
import okhttp3.OkHttpClient
import org.gradle.api.DefaultTask
import org.gradle.api.Incubating
import org.gradle.api.Project
Expand Down Expand Up @@ -57,6 +57,8 @@ abstract class BaseStagingTask extends DefaultTask {
@Incubating
final Property<String> stagingRepositoryId

private SimplifiedHttpJsonRestClient restClient

@Inject
BaseStagingTask(Project project, NexusStagingExtension extension) {
ObjectFactory objectFactory = project.getObjects();
Expand All @@ -65,8 +67,11 @@ abstract class BaseStagingTask extends DefaultTask {
}

@PackageScope
SimplifiedHttpJsonRestClient createClient() {
return new SimplifiedHttpJsonRestClient(new RESTClient(), getUsername(), getPassword())
synchronized SimplifiedHttpJsonRestClient createClient() {
if (restClient == null) {
restClient = new SimplifiedHttpJsonRestClient(new OkHttpClient(), getUsername(), getPassword())
}
return restClient
}

protected StagingProfileFetcher createProfileFetcherWithGivenClient(SimplifiedHttpJsonRestClient client) {
Expand Down
Expand Up @@ -5,7 +5,7 @@ import groovy.transform.CompileStatic
/**
* Custom exception to propagate server errors.
*
* Created as groovyx.net.http.HttpResponseException contains in a message only a reason phrase (e.g. Server Error) without response body
* Created as OkHttp does not throw exceptions for HTTP errors (e.g. Server Error)
* which in many cases is crucial to determine the resons why error was returned.
*
* It may be made redundant once migrated to other HTTP library.
Expand All @@ -15,8 +15,8 @@ class NexusHttpResponseException extends NexusStagingException {

final int statusCode

NexusHttpResponseException(int statusCode, String message, Throwable cause) {
super(message, cause)
NexusHttpResponseException(int statusCode, String message) {
super(message)
this.statusCode = statusCode
}
}
@@ -1,77 +1,84 @@
package io.codearte.gradle.nexus.infra

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import groovyx.net.http.ContentType
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.HttpResponseException
import groovyx.net.http.RESTClient
import java.util.concurrent.TimeUnit
import javax.annotation.concurrent.ThreadSafe
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.jetbrains.annotations.NotNull

/**
* Specialized REST client to communicate with Nexus.
*
* Note: The same as RESTClient from HTTP Builder this class is not thread-safe.
szpak marked this conversation as resolved.
Show resolved Hide resolved
*/
@CompileStatic
@Slf4j
@ThreadSafe
class SimplifiedHttpJsonRestClient {

private enum RequestType {
GET, POST
}
private final OkHttpClient restClient

SimplifiedHttpJsonRestClient(OkHttpClient restClient, String username, String password) {
OkHttpClient.Builder clientBuilder = restClient.newBuilder()
.connectTimeout(60, TimeUnit.SECONDS)
szpak marked this conversation as resolved.
Show resolved Hide resolved
.readTimeout(60, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
clientBuilder.addInterceptor(new Interceptor() {
@Override
Response intercept(@NotNull Interceptor.Chain chain) throws IOException {
Request.Builder request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any ContentType-like class in OkHttp that could be used to get that "application/json" string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately seems to only support parsing with no list of constants

https://square.github.io/okhttp/4.x/okhttp/okhttp3/-media-type/#companion-object-functions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, in that case maybe just a constant String in that class?


private final RESTClient restClient
szpak marked this conversation as resolved.
Show resolved Hide resolved
private final String username
private final String password
if (username != null && password != null) {
request.header("Authorization", Credentials.basic(username, password))
}

SimplifiedHttpJsonRestClient(RESTClient restClient, String username, String password) {
this.restClient = restClient
this.username = username
this.password = password
// params.requestContentType = ContentType.JSON //Does not set Content-Type header as required by WireMock
restClient.headers["Content-Type"] = "application/json" //reported as error in Idea (effectively works fine)
// restClient.headers.put("Content-Type", "application/json")
return chain.proceed(request.build())
}
})
this.restClient = clientBuilder.build()
}

Map get(String uri) {
return (Map) sendRequestHandlingErrors(uri, null, restClient.&get, RequestType.GET).data
Request request = new Request.Builder()
.url(uri)
.build()
return sendRequestHandlingErrors(request)
}

Map post(String uri, Map content) {
return (Map) sendRequestHandlingErrors(uri, content, restClient.&post, RequestType.POST).data
Request request = new Request.Builder()
.post(RequestBody.create(JsonOutput.toJson(content), null))
.url(uri)
.build()
return sendRequestHandlingErrors(request)
}

private HttpResponseDecorator sendRequestHandlingErrors(String uri, Map content, Closure<Object> clientMethodHandler, RequestType requestTypeName) {
private Map sendRequestHandlingErrors(Request request) {
try {
return prepareAndSendRequest(uri, content, clientMethodHandler, requestTypeName)
} catch (HttpResponseException e) {
HttpResponseDecorator resp = e.getResponse();
String message = "${resp.statusLine.statusCode}: ${resp.statusLine.reasonPhrase}, body: ${resp.data ?: '<empty>'}"
//TODO: Suppress error message on 404 if waiting for drop?
log.warn("$requestTypeName request failed. ${message}")
throw new NexusHttpResponseException(e.getStatusCode(), message, e)
}
}

private HttpResponseDecorator prepareAndSendRequest(String uri, Map content, Closure<Object> clientMethodHandler, RequestType requestType) {
setUriAndAuthentication(uri)
Map params = createAndInitializeCallParametersMap()
if (content != null) {
params.body = content
log.debug("$requestType request content: $content")
return restClient.newCall(request).execute().withCloseable {
if (!it.successful) {
String message = "${it.code()}: ${it.message()}, body: ${it.body().string() ?: '<empty>'}"
//TODO: Suppress error message on 404 if waiting for drop?
log.warn("${request.method()} request failed. ${message}")
throw new NexusHttpResponseException(it.code(), message)
}
if (it.body().contentLength() == 0) {
return [:]
} else {
(Map) new JsonSlurper().parse(it.body().byteStream())
}
}
} catch(IOException e) {
throw new NexusStagingException("Could not connect to Nexus.", e)
}
log.debug("$requestType request URL: $uri")
return (HttpResponseDecorator) clientMethodHandler(params)
}

private void setUriAndAuthentication(String uri) {
restClient.uri = uri
if (username != null) {
restClient.auth.basic(username, password) //has to be after URI is set
}
}

private Map createAndInitializeCallParametersMap() { //New for every call - it is cleared up after call by RESTClient
return [contentType: ContentType.JSON]
}
}
Expand Up @@ -42,7 +42,7 @@ class RepositoryStateFetcherSpec extends BaseOperationExecutorSpec implements Fe
def "should map 404 error from server to NOT_FOUND state"() {
given:
client.get(getGetRepositoryStateFullUrlForRepoId(TEST_REPOSITORY_ID)) >> {
throw new NexusHttpResponseException(404, "404: Not Found, body: ${aNotFoundRepo(TEST_REPOSITORY_ID)}", null)
throw new NexusHttpResponseException(404, "404: Not Found, body: ${aNotFoundRepo(TEST_REPOSITORY_ID)}")
}
when:
RepositoryState repoState = repoStateFetcher.getNonTransitioningRepositoryStateById(TEST_REPOSITORY_ID)
Expand Down