summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-client/src
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-03-07 17:56:48 +0100
committerFabian Mastenbroek <mail.fabianm@gmail.com>2022-04-04 12:48:05 +0200
commitabac46fe742484c6e0b90bebe3c86d88231540b2 (patch)
tree9b6d6f46cfc6215e8cd6140fef3e0d24bfbcdc87 /opendc-web/opendc-web-client/src
parent98273d483e68e333f9bf5c39510f9a46f3f3a74f (diff)
feat(web/client): Add separate web client implementation
This change implements a simple client for the OpenDC REST API into a separate module. This allows other users to use this module as well.
Diffstat (limited to 'opendc-web/opendc-web-client/src')
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt73
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt58
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt52
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt63
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt36
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt66
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt42
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt40
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt140
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt54
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt62
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt40
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt43
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt47
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt59
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt145
-rw-r--r--opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt50
17 files changed, 1070 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt
new file mode 100644
index 00000000..33f2b41e
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.auth.AuthController
+import org.opendc.web.client.transport.HttpTransportClient
+import org.opendc.web.client.transport.TransportClient
+import java.net.URI
+
+/**
+ * Client implementation for the user-facing OpenDC REST API (version 2).
+ *
+ * @param client Low-level client for managing the underlying transport.
+ */
+public class OpenDCClient(client: TransportClient) {
+ /**
+ * Construct a new [OpenDCClient].
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ */
+ public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth))
+
+ /**
+ * A resource for the available projects.
+ */
+ public val projects: ProjectResource = ProjectResource(client)
+
+ /**
+ * A resource for the topologies available to the user.
+ */
+ public val topologies: TopologyResource = TopologyResource(client)
+
+ /**
+ * A resource for the portfolios available to the user.
+ */
+ public val portfolios: PortfolioResource = PortfolioResource(client)
+
+ /**
+ * A resource for the scenarios available to the user.
+ */
+ public val scenarios: ScenarioResource = ScenarioResource(client)
+
+ /**
+ * A resource for the available schedulers.
+ */
+ public val schedulers: SchedulerResource = SchedulerResource(client)
+
+ /**
+ * A resource for the available workload traces.
+ */
+ public val traces: TraceResource = TraceResource(client)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt
new file mode 100644
index 00000000..399804e8
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Portfolio
+
+/**
+ * A resource representing the portfolios available to the user.
+ */
+public class PortfolioResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all portfolios that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Portfolio> = client.get("projects/$project/portfolios") ?: emptyList()
+
+ /**
+ * Obtain the portfolio for [project] with [number].
+ */
+ public fun get(project: Long, number: Int): Portfolio? = client.get("projects/$project/portfolios/$number")
+
+ /**
+ * Create a new portfolio for [project] with the specified [request].
+ */
+ public fun create(project: Long, request: Portfolio.Create): Portfolio {
+ return checkNotNull(client.post("projects/$project/portfolios", request))
+ }
+
+ /**
+ * Delete the portfolio for [project] with [index].
+ */
+ public fun delete(project: Long, index: Int): Portfolio {
+ return requireNotNull(client.delete("projects/$project/portfolios/$index")) { "Unknown portfolio $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt
new file mode 100644
index 00000000..12635b89
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Project
+
+/**
+ * A resource representing the projects available to the user.
+ */
+public class ProjectResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all projects available to the user.
+ */
+ public fun getAll(): List<Project> = client.get("projects") ?: emptyList()
+
+ /**
+ * Obtain the project with [id].
+ */
+ public fun get(id: Long): Project? = client.get("projects/$id")
+
+ /**
+ * Create a new project.
+ */
+ public fun create(name: String): Project = checkNotNull(client.post("projects", Project.Create(name)))
+
+ /**
+ * Delete the project with the specified [id].
+ */
+ public fun delete(id: Long): Project = requireNotNull(client.delete("projects/$id")) { "Unknown project $id" }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt
new file mode 100644
index 00000000..7055e752
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Scenario
+
+/**
+ * A resource representing the scenarios available to the user.
+ */
+public class ScenarioResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all scenarios that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Scenario> = client.get("projects/$project/scenarios") ?: emptyList()
+
+ /**
+ * List all scenarios that belong to the specified [portfolioNumber].
+ */
+ public fun getAll(project: Long, portfolioNumber: Int): List<Scenario> = client.get("projects/$project/portfolios/$portfolioNumber/scenarios") ?: emptyList()
+
+ /**
+ * Obtain the scenario for [project] with [index].
+ */
+ public fun get(project: Long, index: Int): Scenario? = client.get("projects/$project/scenarios/$index")
+
+ /**
+ * Create a new scenario for [portfolio][portfolioNumber] with the specified [request].
+ */
+ public fun create(project: Long, portfolioNumber: Int, request: Scenario.Create): Scenario {
+ return checkNotNull(client.post("projects/$project/portfolios/$portfolioNumber", request))
+ }
+
+ /**
+ * Delete the scenario for [project] with [index].
+ */
+ public fun delete(project: Long, index: Int): Scenario {
+ return requireNotNull(client.delete("projects/$project/scenarios/$index")) { "Unknown scenario $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt
new file mode 100644
index 00000000..43b72d88
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.transport.TransportClient
+
+/**
+ * A resource representing the schedulers available in the OpenDC instance.
+ */
+public class SchedulerResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all schedulers available.
+ */
+ public fun getAll(): List<String> = client.get("schedulers") ?: emptyList()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt
new file mode 100644
index 00000000..c37ae8da
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.delete
+import org.opendc.web.client.internal.get
+import org.opendc.web.client.internal.post
+import org.opendc.web.client.internal.put
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.user.Topology
+
+/**
+ * A resource representing the topologies available to the user.
+ */
+public class TopologyResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all topologies that belong to the specified [project].
+ */
+ public fun getAll(project: Long): List<Topology> = client.get("projects/$project/topologies") ?: emptyList()
+
+ /**
+ * Obtain the topology for [project] with [index].
+ */
+ public fun get(project: Long, index: Int): Topology? = client.get("projects/$project/topologies/$index")
+
+ /**
+ * Create a new topology for [project] with [request].
+ */
+ public fun create(project: Long, request: Topology.Create): Topology {
+ return checkNotNull(client.post("projects/$project/topologies", request))
+ }
+
+ /**
+ * Update the topology with [index] for [project] using the specified [request].
+ */
+ public fun update(project: Long, index: Int, request: Topology.Update): Topology? {
+ return client.put("projects/$project/topologies/$index", request)
+ }
+
+ /**
+ * Delete the topology for [project] with [index].
+ */
+ public fun delete(project: Long, index: Long): Topology {
+ return requireNotNull(client.delete("projects/$project/topologies/$index")) { "Unknown topology $index" }
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt
new file mode 100644
index 00000000..8201c432
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.Trace
+
+/**
+ * A resource representing the workload traces available in the OpenDC instance.
+ */
+public class TraceResource internal constructor(private val client: TransportClient) {
+ /**
+ * List all workload traces available.
+ */
+ public fun getAll(): List<Trace> = client.get("traces") ?: emptyList()
+
+ /**
+ * Obtain the workload trace with the specified [id].
+ */
+ public fun get(id: Long): Trace? = client.get("traces/$id")
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt
new file mode 100644
index 00000000..a4c66f55
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.auth
+
+import java.net.http.HttpRequest
+
+/**
+ * Helper interface for managing API authentication.
+ */
+public interface AuthController {
+ /**
+ * Inject the authorization token into the specified [request].
+ */
+ public fun injectToken(request: HttpRequest.Builder)
+
+ /**
+ * Refresh the current auth token.
+ */
+ public fun refreshToken()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt
new file mode 100644
index 00000000..7f9cbacd
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.auth
+
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import org.opendc.web.client.internal.OAuthTokenRequest
+import org.opendc.web.client.internal.OAuthTokenResponse
+import org.opendc.web.client.internal.OpenIdConfiguration
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+
+/**
+ * An [AuthController] for OpenID Connect protected APIs.
+ */
+public class OpenIdAuthController(
+ private val domain: String,
+ private val clientId: String,
+ private val clientSecret: String,
+ private val audience: String = "https://api.opendc.org/v2/",
+ private val client: HttpClient = HttpClient.newHttpClient()
+) : AuthController {
+ /**
+ * The Jackson object mapper to convert messages from/to JSON.
+ */
+ private val mapper = jacksonObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ /**
+ * The cached [OpenIdConfiguration].
+ */
+ private val openidConfig: OpenIdConfiguration
+ get() {
+ var openidConfig = _openidConfig
+ if (openidConfig == null) {
+ openidConfig = requestConfig()
+ _openidConfig = openidConfig
+ }
+
+ return openidConfig
+ }
+ private var _openidConfig: OpenIdConfiguration? = null
+
+ /**
+ * The cached OAuth token.
+ */
+ private var _token: OAuthTokenResponse? = null
+
+ override fun injectToken(request: HttpRequest.Builder) {
+ var token = _token
+ if (token == null) {
+ token = requestToken()
+ _token = token
+ }
+
+ request.header("Authorization", "Bearer ${token.accessToken}")
+ }
+
+ /**
+ * Refresh the current access token.
+ */
+ override fun refreshToken() {
+ val refreshToken = _token?.refreshToken
+ if (refreshToken == null) {
+ requestToken()
+ return
+ }
+
+ _token = refreshToken(openidConfig, refreshToken)
+ }
+
+ /**
+ * Request the OpenID configuration from the chosen auth domain
+ */
+ private fun requestConfig(): OpenIdConfiguration {
+ val request = HttpRequest.newBuilder(URI("https://$domain/.well-known/openid-configuration"))
+ .GET()
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Request the auth token from the server.
+ */
+ private fun requestToken(openidConfig: OpenIdConfiguration): OAuthTokenResponse {
+ val body = OAuthTokenRequest.ClientCredentials(audience, clientId, clientSecret)
+ val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Helper method to refresh the auth token.
+ */
+ private fun refreshToken(openidConfig: OpenIdConfiguration, refreshToken: String): OAuthTokenResponse {
+ val body = OAuthTokenRequest.RefreshToken(refreshToken, clientId, clientSecret)
+ val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+ return mapper.readValue(response.body())
+ }
+
+ /**
+ * Fetch a new access token.
+ */
+ private fun requestToken(): OAuthTokenResponse {
+ val token = requestToken(openidConfig)
+ _token = token
+ return token
+ }
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt
new file mode 100644
index 00000000..29cf09dc
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.core.type.TypeReference
+import org.opendc.web.client.transport.TransportClient
+
+/**
+ * Perform a GET request for resource at [path] and convert to type [T].
+ */
+internal inline fun <reified T> TransportClient.get(path: String): T? {
+ return get(path, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a POST request for resource at [path] and convert to type [T].
+ */
+internal inline fun <B, reified T> TransportClient.post(path: String, body: B): T? {
+ return post(path, body, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a PUT request for resource at [path] and convert to type [T].
+ */
+internal inline fun <B, reified T> TransportClient.put(path: String, body: B): T? {
+ return put(path, body, object : TypeReference<T>() {})
+}
+
+/**
+ * Perform a DELETE request for resource at [path] and convert to type [T].
+ */
+internal inline fun <reified T> TransportClient.delete(path: String): T? {
+ return delete(path, object : TypeReference<T>() {})
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt
new file mode 100644
index 00000000..25341995
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+
+/**
+ * Token request sent to the OAuth server.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "grant_type")
+@JsonSubTypes(
+ value = [
+ JsonSubTypes.Type(value = OAuthTokenRequest.ClientCredentials::class, name = "client_credentials"),
+ JsonSubTypes.Type(value = OAuthTokenRequest.RefreshToken::class, name = "refresh_token")
+ ]
+)
+internal sealed class OAuthTokenRequest {
+ /**
+ * Client credentials grant for OAuth2
+ */
+ data class ClientCredentials(
+ val audience: String,
+ @JsonProperty("client_id")
+ val clientId: String,
+ @JsonProperty("client_secret")
+ val clientSecret: String
+ ) : OAuthTokenRequest()
+
+ /**
+ * Refresh token grant for OAuth2.
+ */
+ data class RefreshToken(
+ @JsonProperty("refresh_token")
+ val refreshToken: String,
+ @JsonProperty("client_id")
+ val clientId: String,
+ @JsonProperty("client_secret")
+ val clientSecret: String
+ ) : OAuthTokenRequest()
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt
new file mode 100644
index 00000000..cd5ccab0
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+/**
+ * Token response from the OAuth server.
+ */
+internal data class OAuthTokenResponse(
+ @JsonProperty("access_token")
+ val accessToken: String,
+ @JsonProperty("refresh_token")
+ val refreshToken: String? = null,
+ @JsonProperty("token_type")
+ val tokenType: String,
+ val scope: String = "",
+ @JsonProperty("expires_in")
+ val expiresIn: Long
+)
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt
new file mode 100644
index 00000000..23fbf368
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.internal
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.net.URI
+
+/**
+ * OpenID configuration exposed by the auth server.
+ */
+internal data class OpenIdConfiguration(
+ val issuer: String,
+ @JsonProperty("authorization_endpoint")
+ val authorizationEndpoint: URI,
+ @JsonProperty("token_endpoint")
+ val tokenEndpoint: URI,
+ @JsonProperty("userinfo_endpoint")
+ val userInfoEndpoint: URI,
+ @JsonProperty("jwks_uri")
+ val jwksUri: URI,
+ @JsonProperty("scopes_supported")
+ val scopesSupported: Set<String>
+)
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt
new file mode 100644
index 00000000..372a92d7
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.runner
+
+import org.opendc.web.client.internal.*
+import org.opendc.web.client.transport.TransportClient
+import org.opendc.web.proto.runner.Job
+
+/**
+ * A resource representing the available simulation jobs for the runner.
+ */
+public class JobResource internal constructor(private val client: TransportClient) {
+ /**
+ * Query the pending jobs.
+ */
+ public fun queryPending(): List<Job> = client.get("jobs") ?: emptyList()
+
+ /**
+ * Obtain the job with [id].
+ */
+ public fun get(id: Long): Job? = client.get("jobs/$id")
+
+ /**
+ * Update the job with [id].
+ */
+ public fun update(id: Long, update: Job.Update): Job? = client.post("jobs/$id", update)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt
new file mode 100644
index 00000000..a3cff6c3
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.runner
+
+import org.opendc.web.client.*
+import org.opendc.web.client.auth.AuthController
+import org.opendc.web.client.transport.HttpTransportClient
+import org.opendc.web.client.transport.TransportClient
+import java.net.URI
+
+/**
+ * Client implementation for the runner-facing OpenDC REST API (version 2).
+ *
+ * @param client Low-level client for managing the underlying transport.
+ */
+public class OpenDCRunnerClient(client: TransportClient) {
+ /**
+ * Construct a new [OpenDCRunnerClient].
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ */
+ public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth))
+
+ /**
+ * A resource for the available simulation jobs.
+ */
+ public val jobs: JobResource = JobResource(client)
+
+ /**
+ * A resource for the available schedulers.
+ */
+ public val schedulers: SchedulerResource = SchedulerResource(client)
+
+ /**
+ * A resource for the available workload traces.
+ */
+ public val traces: TraceResource = TraceResource(client)
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt
new file mode 100644
index 00000000..03b3945f
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.transport
+
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import org.opendc.web.client.auth.AuthController
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.nio.file.Paths
+
+/**
+ * A [TransportClient] that accesses the OpenDC API over HTTP.
+ *
+ * @param baseUrl The base url of the API.
+ * @param auth Helper class for managing authentication.
+ * @param client The HTTP client to use.
+ */
+public class HttpTransportClient(
+ private val baseUrl: URI,
+ private val auth: AuthController,
+ private val client: HttpClient = HttpClient.newHttpClient()
+) : TransportClient {
+ /**
+ * The Jackson object mapper to convert messages from/to JSON.
+ */
+ private val mapper = jacksonObjectMapper()
+ .registerModule(JavaTimeModule())
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ /**
+ * Obtain a resource at [path] of [targetType].
+ */
+ override fun <T> get(path: String, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .GET()
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ get(path, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Update a resource at [path] of [targetType].
+ */
+ override fun <B, T> post(path: String, body: B, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .header("Content-Type", "application/json")
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ post(path, body, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Replace a resource at [path] of [targetType].
+ */
+ override fun <B, T> put(path: String, body: B, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .PUT(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body)))
+ .header("Content-Type", "application/json")
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ put(path, body, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Delete a resource at [path] of [targetType].
+ */
+ override fun <T> delete(path: String, targetType: TypeReference<T>): T? {
+ val request = HttpRequest.newBuilder(buildUri(path))
+ .DELETE()
+ .also { auth.injectToken(it) }
+ .build()
+ val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream())
+
+ return when (val code = response.statusCode()) {
+ in 200..299 -> mapper.readValue(response.body(), targetType)
+ 401 -> {
+ auth.refreshToken()
+ delete(path, targetType)
+ }
+ 404 -> null
+ else -> throw IllegalStateException("Invalid response $code")
+ }
+ }
+
+ /**
+ * Build the absolute [URI] to which the request should be sent.
+ */
+ private fun buildUri(path: String): URI = baseUrl.resolve(Paths.get(baseUrl.path, path).toString())
+}
diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt
new file mode 100644
index 00000000..af727ca7
--- /dev/null
+++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.client.transport
+
+import com.fasterxml.jackson.core.type.TypeReference
+
+/**
+ * Low-level interface for dealing with the transport layer of the API.
+ */
+public interface TransportClient {
+ /**
+ * Obtain a resource at [path] of [targetType].
+ */
+ public fun <T> get(path: String, targetType: TypeReference<T>): T?
+
+ /**
+ * Update a resource at [path] of [targetType].
+ */
+ public fun <B, T> post(path: String, body: B, targetType: TypeReference<T>): T?
+
+ /**
+ * Replace a resource at [path] of [targetType].
+ */
+ public fun <B, T> put(path: String, body: B, targetType: TypeReference<T>): T?
+
+ /**
+ * Delete a resource at [path] of [targetType].
+ */
+ public fun <T> delete(path: String, targetType: TypeReference<T>): T?
+}