Sometimes you may need to parse a query string into a key-value map.
Someone will suggest you to use a library like Apache, but I think it’s too easy task to add a library for it.
Many solutions (on StackOverflow, for example) don’t take into account that URL query string can have multiple values for the same key, so their implementations return Map<String, String> instead of Map<String, Set<String>.
So, let’s see my solution for that problem:
internal interface ParseQueryParams {
fun exec(queryString: String): Map<String, List<String>>
}
internal class ParseQueryParamsImpl @Inject constructor() : ParseQueryParams {
override fun exec(queryString: String): Map<String, List<String>> {
return queryString
.split(QUERY_PARAMS_SEPARATOR)
.mapNotNull { keyAndValue ->
val keyAndValueList = keyAndValue.split(KEY_VALUE_SEPARATOR)
keyAndValueList.takeIf { it.size == 2 }
}
.mapNotNull { keyAndValueList ->
val (key, value) = keyAndValueList
(key to value.trim()).takeIf { value.isNotBlank() } // Don't take empty params
}
.groupBy { (key, _) -> key }
.map { mapEntry ->
mapEntry.key to mapEntry.value.map { it.second }
}
.toMap()
}
private companion object {
private const val QUERY_PARAMS_SEPARATOR = "&"
private const val KEY_VALUE_SEPARATOR = "="
}
}
For example, we will work with this query string: param1=42¶m2=54¶m3=ololo¶m3=trololo¶m4=¶m5
First of all, we split the query string to list of key=value strings:
queryString.split(QUERY_PARAMS_SEPARATOR)
Outcome of this step:
param1=42 param2=54 param3=ololo param3=trololo param4= param5
Next, split key and value and take the result only if it’s two elements long:
.mapNotNull { keyAndValue ->
val keyAndValueList = keyAndValue.split(KEY_VALUE_SEPARATOR)
keyAndValueList.takeIf { it.size == 2 }
}
Outcome of this step:
[param1, 42] [param2, 54] [param3, ololo] [param3, trololo] [param4, ""]
Next, convert list of lists to list of pairs, filtering out blank values and trimming them:
.mapNotNull { keyAndValueList ->
val (key, value) = keyAndValueList
(key to value.trim()).takeIf { value.isNotBlank() }
}
Outcome of this step:
param1 to 42 param2 to 54 param3 to ololo param3 to trololo
Group the values by their keys:
.groupBy { (key, _) -> key }
Outcome of this step:
param1 to (param1 to [42]) param2 to (param2 to [54]) param3 to (param3 to [ololo, trololo])
And then flatten the map values and convert the result to a map:
.map { mapEntry ->
mapEntry.key to mapEntry.value.map { it.second }
}
.toMap()
Outcome of this step:
param1 to [42] param2 to [54] param3 to [ololo, trololo]
Bonus
Here is a JUnit test for this implementation:
internal class ParseQueryParamsTest {
private val parseQueryParams: ParseQueryParams = ParseQueryParamsImpl()
@Test
fun `empty query string`() {
val result = parseQueryParams.exec("")
assertTrue(result.isEmpty())
}
@Test
fun `invalid query string`() {
val result = parseQueryParams.exec("ololo")
assertTrue(result.isEmpty())
}
@Test
fun `valid query string`() {
val result = parseQueryParams.exec("param1=42¶m2=54¶m3=ololo¶m3=trololo¶m4=")
assertEquals(3, result.size)
val param1 = result["param1"]
assertNotNull(param1)
assertEquals(1, param1?.size)
assertEquals("42", param1?.first())
val param2 = result["param2"]
assertNotNull(param2)
assertEquals(1, param2?.size)
assertEquals("54", param2?.first())
val param3 = result["param3"]
assertNotNull(param3)
assertEquals(2, param3?.size)
assertEquals("ololo", param3?.get(0))
assertEquals("trololo", param3?.get(1))
val param4 = result["param4"]
assertNull(param4)
}
}

Leave a Reply