Skip to content

Commit 4d6bc38

Browse files
committed
introduce http client
1 parent 60bc82c commit 4d6bc38

File tree

7 files changed

+93
-84
lines changed

7 files changed

+93
-84
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ dependencies {
3737
compile 'com.beust:klaxon:5.0.5'
3838
compile 'com.neovisionaries:nv-i18n:1.25'
3939

40+
compile group: 'com.google.http-client', name: 'google-http-client', version: '1.23.0'
41+
4042
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
4143
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
4244

src/main/kotlin/com/adamratzman/spotify/Builder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ class SpotifyApiBuilderDsl {
346346
val response = executeTokenRequest(HttpConnection(
347347
url = "https://accounts.spotify.com/api/token",
348348
method = HttpRequestMethod.POST,
349-
body = "grant_type=authorization_code&code=$authorizationCode&redirect_uri=$redirectUri",
349+
bodyMap = null,
350+
bodyString = "grant_type=authorization_code&code=$authorizationCode&redirect_uri=$redirectUri",
350351
contentType = "application/x-www-form-urlencoded",
351352
api = null
352353
), clientId, clientSecret)

src/main/kotlin/com/adamratzman/spotify/SpotifyAPI.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ class SpotifyClientAPI internal constructor(
315315
val response = executeTokenRequest(HttpConnection(
316316
url = "https://accounts.spotify.com/api/token",
317317
method = HttpRequestMethod.POST,
318-
body = "grant_type=refresh_token&refresh_token=${token.refreshToken ?: ""}",
318+
bodyMap = null,
319+
bodyString = "grant_type=refresh_token&refresh_token=${token.refreshToken ?: ""}",
319320
contentType = "application/x-www-form-urlencoded",
320321
api = this
321322
), clientId, clientSecret)
@@ -383,7 +384,8 @@ fun getCredentialedToken(clientId: String, clientSecret: String, api: SpotifyAPI
383384
val response = executeTokenRequest(HttpConnection(
384385
url = "https://accounts.spotify.com/api/token",
385386
method = HttpRequestMethod.POST,
386-
body = "grant_type=client_credentials",
387+
bodyMap = null,
388+
bodyString = "grant_type=client_credentials",
387389
contentType = "application/x-www-form-urlencoded",
388390
api = api
389391
), clientId, clientSecret)

src/main/kotlin/com/adamratzman/spotify/http/Endpoints.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
8787

8888
val responseBody = document.body
8989

90-
document.headers.find { it.key == "Cache-Control" }?.also { cacheControlHeader ->
90+
document.headers.find { it.key.equals("Cache-Control", true) }?.also { cacheControlHeader ->
9191
if (api.useCache) {
9292
cache[spotifyRequest] = (cacheState ?: CacheState(
9393
responseBody, document.headers
94-
.find { it.key == "ETag" }?.value
94+
.find { it.key.equals("ETag", true) }?.value
9595
)).update(cacheControlHeader.value)
9696
}
9797
}
@@ -115,6 +115,7 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
115115
) = HttpConnection(
116116
url,
117117
method,
118+
null,
118119
body,
119120
contentType,
120121
listOf(HttpHeader("Authorization", "Bearer ${api.token.accessToken}")),

src/main/kotlin/com/adamratzman/spotify/http/HttpConnection.kt

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package com.adamratzman.spotify.http
33

44
import com.adamratzman.spotify.SpotifyAPI
55
import com.adamratzman.spotify.models.SpotifyRatelimitedException
6-
import java.io.OutputStreamWriter
7-
import java.net.HttpURLConnection
8-
import java.net.URL
6+
import com.google.api.client.http.ByteArrayContent
7+
import com.google.api.client.http.GenericUrl
8+
import com.google.api.client.http.UrlEncodedContent
9+
import com.google.api.client.http.javanet.NetHttpTransport
910
import java.util.concurrent.TimeUnit
1011

1112
enum class HttpRequestMethod { GET, POST, PUT, DELETE }
@@ -14,38 +15,56 @@ data class HttpHeader(val key: String, val value: String)
1415
internal class HttpConnection(
1516
private val url: String,
1617
private val method: HttpRequestMethod,
17-
private val body: String?,
18+
private val bodyMap: Map<Any, Any>?,
19+
private val bodyString: String?,
1820
private val contentType: String?,
1921
private val headers: List<HttpHeader> = listOf(),
2022
val api: SpotifyAPI? = null
2123
) {
2224

25+
companion object {
26+
private val requestFactory = NetHttpTransport().createRequestFactory()
27+
}
28+
2329
fun execute(additionalHeaders: List<HttpHeader>? = null, retryIf502: Boolean = true): HttpResponse {
24-
val connection = URL(url).openConnection() as HttpURLConnection
25-
connection.requestMethod = method.toString()
30+
val genericUrl = GenericUrl(url)
31+
val request = when (method) {
32+
HttpRequestMethod.GET -> requestFactory.buildGetRequest(genericUrl)
33+
HttpRequestMethod.DELETE -> requestFactory.buildDeleteRequest(genericUrl)
34+
HttpRequestMethod.PUT, HttpRequestMethod.POST -> {
35+
val content = if (contentType == "application/x-www-form-urlencoded") {
36+
bodyMap?.let { body ->
37+
UrlEncodedContent(body.map { it.key.toString() to it.value.toString() }.toMap())
38+
} ?: ByteArrayContent.fromString(contentType, bodyString)
39+
} else bodyString?.let { ByteArrayContent.fromString(contentType, bodyString) }
40+
41+
if (method == HttpRequestMethod.PUT) requestFactory.buildPutRequest(genericUrl, content)
42+
else requestFactory.buildPostRequest(genericUrl, content)
43+
}
44+
}
2645

27-
contentType?.let { connection.setRequestProperty("Content-Type", contentType) }
46+
if (method == HttpRequestMethod.DELETE && bodyString != null) {
2847

29-
((additionalHeaders ?: listOf()) + headers).forEach { (key, value) ->
30-
connection.setRequestProperty(key, value)
48+
//request.content = ByteArrayContent.fromString(contentType, bodyString)
3149
}
3250

33-
if (body != null && method != HttpRequestMethod.GET) {
34-
connection.doOutput = true
35-
// connection.setFixedLengthStreamingMode(body.toByteArray().size)
36-
val os = connection.outputStream
37-
val osw = OutputStreamWriter(os)
38-
osw.write(body)
39-
osw.apply {
40-
flush()
41-
close()
42-
}
43-
os.close()
51+
if (contentType != null && method != HttpRequestMethod.POST) request.headers.contentType = contentType
52+
53+
val allHeaders = (additionalHeaders ?: listOf()) + headers
54+
55+
allHeaders.filter { !it.key.equals("Authorization", true) }.forEach { (key, value) ->
56+
request.headers[key] = value
57+
}
58+
59+
allHeaders.find { it.key.equals("Authorization", true) }?.let { authHeader ->
60+
request.headers.authorization = authHeader.value
4461
}
4562

46-
connection.connect()
63+
request.throwExceptionOnExecuteError = false
4764

48-
val responseCode = connection.responseCode
65+
val response = request.execute()
66+
67+
val responseCode = response.statusCode
4968

5069
if (responseCode == 502 && retryIf502) {
5170
api?.logger?.logError(false, "Received 502 (Invalid response) for URL $url and $this\nRetrying..", null)
@@ -55,28 +74,28 @@ internal class HttpConnection(
5574
if (responseCode == 502 && !retryIf502) api?.logger?.logWarning("502 retry successful for $this")
5675

5776
if (responseCode == 429) {
58-
val ratelimit = connection.getHeaderField("Retry-After").toLong() + 1L
77+
val ratelimit = response.headers["Retry-After"]!!.toString().toLong() + 1L
5978
if (api?.retryWhenRateLimited == true) {
6079
api.logger.logError(false, "The request ($url) was ratelimited for $ratelimit seconds at ${System.currentTimeMillis()}", null)
6180

62-
var response: HttpResponse? = null
81+
var retryResponse: HttpResponse? = null
6382
api.executor.schedule({
64-
response = try {
83+
retryResponse = try {
6584
execute(additionalHeaders, retryIf502 = retryIf502)
6685
} catch (e: Throwable) {
6786
throw e
6887
}
6988
}, ratelimit, TimeUnit.SECONDS).get()
7089

71-
return response!!
90+
return retryResponse!!
7291
} else throw SpotifyRatelimitedException(ratelimit)
7392
}
7493

75-
val body = (connection.errorStream ?: connection.inputStream).bufferedReader().use {
76-
val text = it.readText()
77-
it.close()
78-
text
79-
}
94+
val contentReader = response.content.bufferedReader()
95+
val body = contentReader.readText()
96+
97+
contentReader.close()
98+
response.content.close()
8099

81100
if (responseCode == 401 && body.contains("access token") &&
82101
api != null && api.automaticRefresh) {
@@ -87,19 +106,19 @@ internal class HttpConnection(
87106
return HttpResponse(
88107
responseCode = responseCode,
89108
body = body,
90-
headers = connection.headerFields.keys.filterNotNull().map { HttpHeader(it, connection.getHeaderField(it)) }
91-
).also { connection.disconnect() }
109+
headers = response.headers.map { HttpHeader(it.key, it.value?.toString() ?: "null") }
110+
).also { response.disconnect() }
92111
}
93112

94113
override fun toString(): String {
95114
return """HttpConnection (
96115
|url=$url,
97116
|method=$method,
98-
|body=$body,
117+
|body=${bodyString ?: bodyMap},
99118
|contentType=$contentType,
100119
|headers=${headers.toList()}
101120
| )""".trimMargin()
102121
}
103122
}
104123

105-
internal data class HttpResponse(val responseCode: Int, val body: String, val headers: List<HttpHeader>)
124+
internal data class HttpResponse(val responseCode: Int, val body: String, val headers: List<HttpHeader>)

src/main/kotlin/com/adamratzman/spotify/models/Authentication.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ data class Token(
2121
@Json(name = "token_type") val tokenType: String,
2222
@Json(name = "expires_in") val expiresIn: Int,
2323
@Json(name = "refresh_token") val refreshToken: String? = null,
24-
@Json(ignored = false) private val scopeString: String? = null,
24+
@Json(ignored = false, name="scope") private val scopeString: String? = null,
2525
@Json(ignored = true) val scopes: List<SpotifyScope> = scopeString?.let { str ->
2626
str.split(" ").mapNotNull { scope -> SpotifyScope.values().find { it.uri.equals(scope, true) } }
2727
} ?: listOf()

src/test/kotlin/com/adamratzman/spotify/utilities/HttpConnectionTests.kt

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.adamratzman.spotify.http.HttpConnection
66
import com.adamratzman.spotify.http.HttpRequestMethod
77
import org.json.JSONObject
88
import org.junit.jupiter.api.Assertions.assertEquals
9+
import org.junit.jupiter.api.Assertions.assertTrue
910
import org.spekframework.spek2.Spek
1011
import org.spekframework.spek2.style.specification.describe
1112

@@ -16,6 +17,7 @@ class HttpConnectionTests : Spek({
1617
"https://httpbin.org/get?query=string",
1718
HttpRequestMethod.GET,
1819
null,
20+
null,
1921
"text/html"
2022
).execute().let { it to JSONObject(it.body) }
2123

@@ -25,15 +27,16 @@ class HttpConnectionTests : Spek({
2527

2628
it("get request header") {
2729
val requestHeader = body.getJSONObject("headers")
28-
assertEquals(
29-
mapOf(
30-
"Accept" to "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
31-
"Host" to "httpbin.org",
32-
"Content-Type" to "text/html"
33-
).toSortedMap(),
34-
// ignore the user-agent because of the version in it
35-
requestHeader.toMap().filterKeys { it.length >= 3 && it != "User-Agent" }.toSortedMap()
36-
)
30+
assertTrue {
31+
// ignore the user-agent because of the version in it
32+
requestHeader.toMap().toList().containsAll(
33+
mapOf(
34+
"Accept" to "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
35+
"Host" to "httpbin.org",
36+
"Content-Type" to "text/html"
37+
).toList()
38+
)
39+
}
3740
}
3841

3942
it("get request query string") {
@@ -45,6 +48,7 @@ class HttpConnectionTests : Spek({
4548
val (response, body) = HttpConnection(
4649
"https://httpbin.org/post?query=string",
4750
HttpRequestMethod.POST,
51+
null,
4852
"body",
4953
"text/html"
5054
).execute().let { it to JSONObject(it.body) }
@@ -55,16 +59,16 @@ class HttpConnectionTests : Spek({
5559

5660
it("post request header") {
5761
val requestHeader = body.getJSONObject("headers")
58-
assertEquals(
59-
mapOf(
60-
"Accept" to "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
61-
"Host" to "httpbin.org",
62-
"Content-Type" to "text/html",
63-
"Content-Length" to "4"
64-
).toSortedMap(),
65-
// ignore the user-agent because of the version in it
66-
requestHeader.toMap().filterKeys { it.length >= 4 && it != "User-Agent" }.toSortedMap()
67-
)
62+
assertTrue {
63+
requestHeader.toMap().toList().containsAll(
64+
mapOf(
65+
"Accept" to "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
66+
"Host" to "httpbin.org",
67+
"Content-Type" to "text/html",
68+
"Content-Length" to "4"
69+
).toList()
70+
)
71+
}
6872
}
6973

7074
it("post request query string") {
@@ -80,35 +84,14 @@ class HttpConnectionTests : Spek({
8084
val (response, body) = HttpConnection(
8185
"https://httpbin.org/delete?query=string",
8286
HttpRequestMethod.DELETE,
83-
"body",
87+
null,
88+
null,
8489
"text/html"
8590
).execute().let { it to JSONObject(it.body) }
8691

8792
it("delete request response code") {
8893
assertEquals(200, response.responseCode)
8994
}
90-
91-
it("delete request header") {
92-
val requestHeader = body.getJSONObject("headers")
93-
assertEquals(
94-
mapOf(
95-
"Accept" to "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
96-
"Host" to "httpbin.org",
97-
"Content-Type" to "text/html",
98-
"Content-Length" to "4"
99-
).toSortedMap(),
100-
// ignore the user-agent because of the version in it
101-
requestHeader.toMap().filterKeys { it.length >= 4 && it != "User-Agent" }.toSortedMap()
102-
)
103-
}
104-
105-
it("delete request query string") {
106-
assertEquals("string", body.getJSONObject("args").getString("query"))
107-
}
108-
109-
it("delete request body") {
110-
assertEquals("body", body.getString("data"))
111-
}
11295
}
11396

11497
it("status code") {
@@ -118,6 +101,7 @@ class HttpConnectionTests : Spek({
118101
"https://apple.com",
119102
HttpRequestMethod.GET,
120103
null,
104+
null,
121105
null
122106
).execute().responseCode
123107
)
@@ -127,7 +111,7 @@ class HttpConnectionTests : Spek({
127111
api.useCache = false
128112
api.retryWhenRateLimited = true
129113
api.clearCache()
130-
(1..250).forEach {
114+
for (it in 1..250) {
131115
api.tracks.getTrack("5OT3k9lPxI2jkaryRK3Aop").complete()
132116
}
133117
api.useCache = true

0 commit comments

Comments
 (0)