Skip to content

Commit

Permalink
Add Java HTTP client adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
jaguililla committed Jun 11, 2024
1 parent 77f1d8d commit e96bbee
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 0 deletions.
31 changes: 31 additions & 0 deletions http/http_client_java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# Module http_client_java
[http_client] implementation using the [Java HTTP Client] classes.

[http_client]: /http_client
[Java HTTP Client]: https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html

### Install the Dependency

=== "build.gradle"

```groovy
repositories {
mavenCentral()
}

implementation("com.hexagonkt:http_client_java:$hexagonVersion")
```

=== "pom.xml"

```xml
<dependency>
<groupId>com.hexagonkt</groupId>
<artifactId>http_client_java</artifactId>
<version>$hexagonVersion</version>
</dependency>
```

# Package com.hexagonkt.http.client.java
Java HTTP client implementation classes.
16 changes: 16 additions & 0 deletions http/http_client_java/api/http_client_java.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
public class com/hexagonkt/http/client/java/JavaClientAdapter : com/hexagonkt/http/client/HttpClientPort {
protected field httpClient Lcom/hexagonkt/http/client/HttpClient;
protected field jettyClient Ljava/net/http/HttpClient;
public fun <init> ()V
protected final fun getHttpClient ()Lcom/hexagonkt/http/client/HttpClient;
protected final fun getJettyClient ()Ljava/net/http/HttpClient;
public fun send (Lcom/hexagonkt/http/model/HttpRequestPort;)Lcom/hexagonkt/http/model/HttpResponsePort;
protected final fun setHttpClient (Lcom/hexagonkt/http/client/HttpClient;)V
protected final fun setJettyClient (Ljava/net/http/HttpClient;)V
public fun shutDown ()V
public fun sse (Lcom/hexagonkt/http/model/HttpRequestPort;)Ljava/util/concurrent/Flow$Publisher;
public fun startUp (Lcom/hexagonkt/http/client/HttpClient;)V
public fun started ()Z
public fun ws (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;)Lcom/hexagonkt/http/model/ws/WsSession;
}

16 changes: 16 additions & 0 deletions http/http_client_java/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

plugins {
id("java-library")
}

apply(from = "$rootDir/gradle/kotlin.gradle")
apply(from = "$rootDir/gradle/publish.gradle")
apply(from = "$rootDir/gradle/dokka.gradle")
apply(from = "$rootDir/gradle/native.gradle")
apply(from = "$rootDir/gradle/detekt.gradle")

description = "HTTP client adapter for the Java HTTP client."

dependencies {
"api"(project(":http:http_client"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package com.hexagonkt.http.client.java

import com.hexagonkt.http.client.HttpClient
import com.hexagonkt.http.client.HttpClientPort
import com.hexagonkt.http.client.HttpClientSettings
import com.hexagonkt.http.model.HttpResponse
import com.hexagonkt.http.model.*
import com.hexagonkt.http.model.ws.WsSession
import java.lang.UnsupportedOperationException
import java.net.http.HttpClient.Redirect.ALWAYS
import java.net.http.HttpClient.Redirect.NEVER
import java.util.concurrent.Flow.Publisher
import java.net.http.HttpClient as JavaHttpClient

/**
* Client to use other REST services.
*/
open class JavaClientAdapter : HttpClientPort {

protected lateinit var jettyClient: JavaHttpClient
protected lateinit var httpClient: HttpClient
private lateinit var httpSettings: HttpClientSettings
private var started: Boolean = false

override fun startUp(client: HttpClient) {
val settings = client.settings

httpClient = client
httpSettings = settings
jettyClient = JavaHttpClient
.newBuilder()
.followRedirects(if (settings.followRedirects) ALWAYS else NEVER)
// .sslContext(sslContext(settings))
.build()

started = true
}

override fun shutDown() {
started = false
}

override fun started() =
started

override fun send(request: HttpRequestPort): HttpResponsePort {
// val response =
// try {
// createJettyRequest(jettyClient, request).send()
// }
// catch (e: ExecutionException) {
// val cause = e.cause
// if (cause is HttpResponseException) cause.response
// else throw e
// }
//
// return convertJettyResponse(httpClient, jettyClient, response)
return HttpResponse()
}

override fun ws(
path: String,
onConnect: WsSession.() -> Unit,
onBinary: WsSession.(data: ByteArray) -> Unit,
onText: WsSession.(text: String) -> Unit,
onPing: WsSession.(data: ByteArray) -> Unit,
onPong: WsSession.(data: ByteArray) -> Unit,
onClose: WsSession.(status: Int, reason: String) -> Unit,
): WsSession {
throw UnsupportedOperationException("WebSockets not supported")
}

override fun sse(request: HttpRequestPort): Publisher<ServerEvent> {
throw UnsupportedOperationException("SSE not supported")
}

// private fun convertJettyResponse(
// adapterHttpClient: HttpClient, adapterJettyClient: JettyHttpClient, response: Response
// ): HttpResponse {
//
// val bodyString = if (response is ContentResponse) response.contentAsString else ""
//
// if (httpSettings.useCookies)
// adapterHttpClient.cookies = adapterJettyClient.httpCookieStore.all().map {
// Cookie(
// it.name,
// it.value,
// it.maxAge,
// it.isSecure,
// it.path,
// it.isHttpOnly,
// it.domain,
// it.attributes["SameSite"]?.uppercase()?.let(CookieSameSite::valueOf),
// expires = it.expires,
// )
// }
//
// return HttpResponse(
// body = bodyString,
// headers = convertHeaders(response.headers),
// contentType = response.headers["content-type"]?.let { parseContentType(it) },
// cookies = adapterHttpClient.cookies,
// status = HttpStatus(response.status),
// contentLength = bodyString.length.toLong(),
// )
// }
//
// private fun convertHeaders(headers: HttpFields): Headers =
// Headers(
// headers
// .fieldNamesCollection
// .map { it.lowercase() }
// .filter { it !in CHECKED_HEADERS }
// .map { Header(it, headers.getValuesList(it)) }
// )
//
// private fun createJettyRequest(
// adapterJettyClient: JettyHttpClient, request: HttpRequestPort
// ): Request {
//
// val contentType = request.contentType
// val authorization = request.authorization
// val baseUrl = httpSettings.baseUrl
//
// if (httpSettings.useCookies) {
// val uri = (baseUrl ?: request.url()).toURI()
// addCookies(uri, adapterJettyClient.httpCookieStore, request.cookies)
// }
//
// val jettyRequest = adapterJettyClient
// .newRequest(URI((baseUrl?.toString() ?: "") + request.path))
// .method(HttpMethod.valueOf(request.method.toString()))
// .headers {
// it.remove("accept-encoding") // Don't send encoding by default
// if (contentType != null)
// it.put("content-type", contentType.text)
// if (authorization != null)
// it.put("authorization", authorization.text)
// request.headers.values.forEach { (k, v) ->
// v.map(Any::toString).forEach { s -> it.add(k, s)}
// }
// }
// .body(createBody(request))
// .accept(*request.accept.map { it.text }.toTypedArray())
//
// request.queryParameters
// .forEach { (k, v) -> v.strings().forEach { jettyRequest.param(k, it) } }
//
// return jettyRequest
// }
//
// private fun createBody(request: HttpRequestPort): Request.Content {
//
// if (request.parts.isEmpty() && request.formParameters.isEmpty())
// return BytesRequestContent(bodyToBytes(request.body))
//
// val multiPart = MultiPartRequestContent()
//
// request.parts.forEach { p ->
// if (p.submittedFileName == null)
// // TODO Add content type if present
// multiPart.addPart(
// ContentSourcePart(p.name, null, EMPTY, StringRequestContent(p.bodyString()))
// )
// else
// multiPart.addPart(
// ContentSourcePart(
// p.name,
// p.submittedFileName,
// EMPTY,
// BytesRequestContent(bodyToBytes(p.body)),
// )
// )
// }
//
// request.formParameters
// .forEach { (k, v) ->
// v.strings().forEach {
// multiPart.addPart(ContentSourcePart(k, null, EMPTY, StringRequestContent(it)))
// }
// }
//
// multiPart.close()
//
// return multiPart
// }
//
// private fun addCookies(uri: URI, store: HttpCookieStore, cookies: List<Cookie>) {
// cookies.forEach {
// val httpCookie = java.net.HttpCookie(it.name, it.value)
// httpCookie.secure = it.secure
// httpCookie.maxAge = it.maxAge
// httpCookie.path = it.path
// httpCookie.isHttpOnly = it.httpOnly
// it.domain?.let(httpCookie::setDomain)
//
// val from = HttpCookie.build(httpCookie).expires(it.expires)
//
// it.sameSite?.let { ss ->
// when(ss){
// STRICT -> SameSite.STRICT
// LAX -> SameSite.LAX
// NONE -> SameSite.NONE
// }
// }?.let { ss -> from.sameSite(ss) }
//
// store.add(uri, from.build())
// }
// }
//
// private fun sslContext(settings: HttpClientSettings): SSLContext =
// when {
// settings.insecure ->
// ClientSslContextFactory().apply { isTrustAll = true }
//
// settings.sslSettings != null -> {
// val sslSettings = settings.sslSettings ?: error("SSL settings cannot be 'null'")
// val keyStore = sslSettings.keyStore
// val trustStore = sslSettings.trustStore
// val sslContextBuilder = ClientSslContextFactory()
//
// if (keyStore != null) {
// val store = loadKeyStore(keyStore, sslSettings.keyStorePassword)
// sslContextBuilder.keyStore = store
// sslContextBuilder.keyStorePassword = sslSettings.keyStorePassword
// }
//
// if (trustStore != null) {
// val store = loadKeyStore(trustStore, sslSettings.trustStorePassword)
// sslContextBuilder.trustStore = store
// sslContextBuilder.setTrustStorePassword(sslSettings.trustStorePassword)
// }
//
// sslContextBuilder
// }
//
// else ->
// ClientSslContextFactory()
// }
}
9 changes: 9 additions & 0 deletions http/http_client_java/src/main/kotlin/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

module com.hexagonkt.http_client_java {

requires transitive com.hexagonkt.http;
requires transitive com.hexagonkt.http_client;
requires transitive java.net.http;

exports com.hexagonkt.http.client.java;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.hexagonkt.http.client.java

import com.hexagonkt.http.client.HttpClient
import com.hexagonkt.http.model.HttpRequest
import org.junit.jupiter.api.Test
import kotlin.IllegalStateException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

internal class JavaClientAdapterTest {

@Test fun `Send request without starting client`() {
val client = HttpClient(JavaClientAdapter())
val request = HttpRequest()
val message = assertFailsWith<IllegalStateException> { client.send(request) }.message
val expectedMessage = "HTTP client *MUST BE STARTED* before sending requests"
assertEquals(expectedMessage, message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Args= \
--initialize-at-build-time=kotlin.annotation.AnnotationRetention \
--initialize-at-build-time=kotlin.annotation.AnnotationTarget

0 comments on commit e96bbee

Please sign in to comment.