Skip to content

Commit e66e855

Browse files
Go back to guava MediaType (#6)
This is done because it can handle wildcards. Apache http ContentType did not correctly handle multiple media types and it could not handle wildcards. Multiple media types in an accept header are now handled by splitting the header value.
1 parent 16635d4 commit e66e855

File tree

8 files changed

+50
-34
lines changed

8 files changed

+50
-34
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists

router-protobuf/src/main/kotlin/io/moia/router/proto/ProtoSerializationHandler.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package io.moia.router.proto
22

3+
import com.google.common.net.MediaType
34
import com.google.protobuf.GeneratedMessageV3
45
import io.moia.router.ResponseEntity
56
import io.moia.router.SerializationHandler
6-
import org.apache.http.entity.ContentType
77
import java.util.Base64
88

99
class ProtoSerializationHandler : SerializationHandler {
1010

11-
private val json = ContentType.parse("application/json")
11+
private val json = MediaType.parse("application/json")
1212

13-
override fun supports(acceptHeader: ContentType, response: ResponseEntity<*>): Boolean =
13+
override fun supports(acceptHeader: MediaType, response: ResponseEntity<*>): Boolean =
1414
response.body is GeneratedMessageV3
1515

16-
override fun serialize(acceptHeader: ContentType, response: ResponseEntity<*>): String {
16+
override fun serialize(acceptHeader: MediaType, response: ResponseEntity<*>): String {
1717
val message = response.body as GeneratedMessageV3
18-
return if (json.mimeType == acceptHeader.mimeType) {
18+
return if (acceptHeader.`is`(json)) {
1919
ProtoBufUtils.toJsonWithoutWrappers(message)
2020
} else {
2121
Base64.getEncoder().encodeToString(message.toByteArray())

router/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ dependencies {
99
compile("org.slf4j:slf4j-api:1.7.26")
1010
compile("com.fasterxml.jackson.core:jackson-databind:2.9.8")
1111
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8")
12-
compile("org.apache.httpcomponents:httpcore:4.4.11")
12+
compile("com.google.guava:guava:23.0")
1313

1414
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.4.0")
1515
testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.0")

router/src/main/kotlin/io/moia/router/DeserializationHandler.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package io.moia.router
33
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
44
import com.fasterxml.jackson.databind.ObjectMapper
55
import com.fasterxml.jackson.databind.type.TypeFactory
6-
import org.apache.http.entity.ContentType
6+
import com.google.common.net.MediaType
77
import kotlin.reflect.KClass
88
import kotlin.reflect.KType
99
import kotlin.reflect.full.isSubclassOf
@@ -27,10 +27,10 @@ class DeserializationHandlerChain(private val handlers: List<DeserializationHand
2727

2828
class JsonDeserializationHandler(private val objectMapper: ObjectMapper) : DeserializationHandler {
2929

30-
private val json = ContentType.parse("application/json")
30+
private val json = MediaType.parse("application/json")
3131

3232
override fun supports(input: APIGatewayProxyRequestEvent) =
33-
input.contentType() != null && ContentType.parse(input.contentType()).mimeType == json.mimeType
33+
input.contentType() != null && MediaType.parse(input.contentType()!!).`is`(json)
3434

3535
override fun deserialize(input: APIGatewayProxyRequestEvent, target: KType?): Any? {
3636
val targetClass = target?.classifier as KClass<*>

router/src/main/kotlin/io/moia/router/RequestHandler.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
66
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent
77
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
88
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
9-
import org.apache.http.entity.ContentType
9+
import com.google.common.net.MediaType
1010
import org.slf4j.Logger
1111
import org.slf4j.LoggerFactory
1212
import kotlin.reflect.KClass
@@ -38,7 +38,7 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
3838
val request =
3939
Request(input, requestBody, routerFunction.requestPredicate.pathPattern)
4040
val response = router.filter.then(handler as HandlerFunction<*, *>).invoke(request)
41-
createResponse(input, response)
41+
createResponse(routerFunction.requestPredicate.matchedAcceptType(input.acceptHeader()), input, response)
4242
} catch (e: Exception) {
4343
when (e) {
4444
is ApiException -> createApiExceptionErrorResponse(input, e)
@@ -150,19 +150,18 @@ abstract class RequestHandler : RequestHandler<APIGatewayProxyRequestEvent, APIG
150150
.withHeaders(mapOf("Content-Type" to "application/json"))
151151
}
152152

153-
open fun <T> createResponse(input: APIGatewayProxyRequestEvent, response: ResponseEntity<T>): APIGatewayProxyResponseEvent {
153+
open fun <T> createResponse(contentType: MediaType?, input: APIGatewayProxyRequestEvent, response: ResponseEntity<T>): APIGatewayProxyResponseEvent {
154154
// TODO add default accept type
155-
val accept = ContentType.parse(input.acceptHeader())
156155
return when {
157156
// no-content response
158157
response.body == null && response.statusCode == 204 -> APIGatewayProxyResponseEvent()
159158
.withStatusCode(204)
160159
.withHeaders(response.headers)
161-
serializationHandlerChain.supports(accept, response) ->
160+
serializationHandlerChain.supports(contentType!!, response) ->
162161
APIGatewayProxyResponseEvent()
163162
.withStatusCode(response.statusCode)
164-
.withBody(serializationHandlerChain.serialize(accept, response))
165-
.withHeaders(response.headers + ("Content-Type" to accept.toString()))
163+
.withBody(serializationHandlerChain.serialize(contentType, response))
164+
.withHeaders(response.headers + ("Content-Type" to contentType.toString()))
166165
else -> throw IllegalArgumentException("unsupported response $response")
167166
}
168167
}

router/src/main/kotlin/io/moia/router/RequestPredicate.kt

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.moia.router
22

33
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
4-
import org.apache.http.entity.ContentType
4+
import com.google.common.net.MediaType
55

66
data class RequestPredicate(
77
val method: String,
@@ -34,19 +34,34 @@ data class RequestPredicate(
3434
RequestMatchResult(
3535
matchPath = pathMatches(request),
3636
matchMethod = methodMatches(request),
37-
matchAcceptType = contentTypeMatches(request.acceptHeader(), produces),
38-
matchContentType = contentTypeMatches(request.contentType(), consumes)
37+
matchAcceptType = acceptMatches(request.acceptHeader()),
38+
matchContentType = contentTypeMatches(request.contentType())
3939
)
4040

4141
private fun pathMatches(request: APIGatewayProxyRequestEvent) =
4242
request.path?.let { UriTemplate.from(pathPattern).matches(it) } ?: false
4343
private fun methodMatches(request: APIGatewayProxyRequestEvent) = method.equals(request.httpMethod, true)
44-
private fun contentTypeMatches(contentType: String?, accepted: Set<String>) =
45-
if (accepted.isEmpty() && contentType == null) true
46-
else if (contentType == null) false
47-
else accepted.any { ContentType.parse(contentType).mimeType == ContentType.parse(it).mimeType }
4844

49-
companion object
45+
/**
46+
* Find the media type that is compatible with the one the client requested out of the ones that the the handler can produce
47+
* Talking into account that an accept header can contain multiple media types (e.g. application/xhtml+xml, application/json)
48+
*/
49+
fun matchedAcceptType(acceptType: String?) =
50+
if (produces.isEmpty() || acceptType == null) null
51+
else produces
52+
.map { MediaType.parse(it) }
53+
// find the first media type that can be produced that is compatible with the requested type
54+
.firstOrNull { acceptType.split(",")
55+
.map { p -> p.trim() }
56+
.any { m -> MediaType.parse(m).`is`(it) } }
57+
58+
private fun acceptMatches(contentType: String?) =
59+
matchedAcceptType(contentType) != null
60+
61+
private fun contentTypeMatches(contentType: String?) =
62+
if (consumes.isEmpty() && contentType == null) true
63+
else if (contentType == null) false
64+
else consumes.any { MediaType.parse(contentType).`is`(MediaType.parse(it)) }
5065
}
5166

5267
internal data class RequestMatchResult(
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
package io.moia.router
22

33
import com.fasterxml.jackson.databind.ObjectMapper
4-
import org.apache.http.entity.ContentType
4+
import com.google.common.net.MediaType
55

66
interface SerializationHandler {
77

8-
fun supports(acceptHeader: ContentType, response: ResponseEntity<*>): Boolean
8+
fun supports(acceptHeader: MediaType, response: ResponseEntity<*>): Boolean
99

10-
fun serialize(acceptHeader: ContentType, response: ResponseEntity<*>): String
10+
fun serialize(acceptHeader: MediaType, response: ResponseEntity<*>): String
1111
}
1212

1313
class SerializationHandlerChain(private val handlers: List<SerializationHandler>) :
1414
SerializationHandler {
1515

16-
override fun supports(acceptHeader: ContentType, response: ResponseEntity<*>): Boolean =
16+
override fun supports(acceptHeader: MediaType, response: ResponseEntity<*>): Boolean =
1717
handlers.any { it.supports(acceptHeader, response) }
1818

19-
override fun serialize(acceptHeader: ContentType, response: ResponseEntity<*>): String =
19+
override fun serialize(acceptHeader: MediaType, response: ResponseEntity<*>): String =
2020
handlers.first { it.supports(acceptHeader, response) }.serialize(acceptHeader, response)
2121
}
2222

2323
class JsonSerializationHandler(private val objectMapper: ObjectMapper) : SerializationHandler {
2424

25-
private val json = ContentType.parse("application/json")
25+
private val json = MediaType.parse("application/json")
2626

27-
override fun supports(acceptHeader: ContentType, response: ResponseEntity<*>): Boolean = acceptHeader.mimeType == json.mimeType
27+
override fun supports(acceptHeader: MediaType, response: ResponseEntity<*>): Boolean = acceptHeader.`is`(json)
2828

29-
override fun serialize(acceptHeader: ContentType, response: ResponseEntity<*>): String =
29+
override fun serialize(acceptHeader: MediaType, response: ResponseEntity<*>): String =
3030
objectMapper.writeValueAsString(response.body)
3131
}

router/src/test/kotlin/io/moia/router/RequestHandlerTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,15 @@ class RequestHandlerTest {
193193
val response = testRequestHandler.handleRequest(
194194
POST("/some")
195195
.withHeaders(mapOf(
196-
"Accept" to "application/json, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8",
196+
"Accept" to "application/xhtml+xml, application/json, application/xml;q=0.9, image/webp, */*;q=0.8",
197197
"Content-Type" to "application/json"
198198
))
199199
.withBody("""{ "greeting": "some" }"""), mockk()
200200
)
201201

202202
assert(response.statusCode).isEqualTo(200)
203+
assert(response.getHeaderCaseInsensitive("content-type")).isEqualTo("application/json")
204+
203205
assert(response.body).isEqualTo("""{"greeting":"some"}""")
204206
}
205207

0 commit comments

Comments
 (0)