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