1 /* <lambda>null2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.navigation 17 18 import androidx.annotation.RestrictTo 19 import androidx.navigation.serialization.generateRoutePattern 20 import androidx.savedstate.SavedState 21 import androidx.savedstate.read 22 import androidx.savedstate.savedState 23 import androidx.savedstate.write 24 import kotlin.jvm.JvmOverloads 25 import kotlin.jvm.JvmStatic 26 import kotlin.jvm.JvmSuppressWildcards 27 import kotlin.reflect.KClass 28 import kotlin.reflect.KType 29 import kotlinx.serialization.InternalSerializationApi 30 import kotlinx.serialization.serializer 31 32 /** 33 * NavDeepLink encapsulates the parsing and matching of a navigation deep link. 34 * 35 * This should be added to a [NavDestination] using [NavDestination.addDeepLink]. 36 */ 37 public class NavDeepLink 38 internal constructor( 39 /** 40 * The uri pattern from the NavDeepLink. 41 * 42 * @see NavDeepLinkRequest.uri 43 */ 44 public val uriPattern: String?, 45 /** 46 * The action from the NavDeepLink. 47 * 48 * @see NavDeepLinkRequest.action 49 */ 50 public val action: String?, 51 /** 52 * The mimeType from the NavDeepLink. 53 * 54 * @see NavDeepLinkRequest.mimeType 55 */ 56 public val mimeType: String? 57 ) { 58 // path 59 private val pathArgs = mutableListOf<String>() 60 private var pathRegex: String? = null 61 private val pathPattern by lazy { pathRegex?.let { Regex(it, RegexOption.IGNORE_CASE) } } 62 63 // query 64 private val isParameterizedQuery by lazy { 65 uriPattern != null && QUERY_PATTERN.matches(uriPattern) 66 } 67 private val queryArgsMap by lazy(LazyThreadSafetyMode.NONE) { parseQuery() } 68 private var isSingleQueryParamValueOnly = false 69 70 // fragment 71 private val fragArgsAndRegex: Pair<MutableList<String>, String>? by 72 lazy(LazyThreadSafetyMode.NONE) { parseFragment() } 73 private val fragArgs by 74 lazy(LazyThreadSafetyMode.NONE) { fragArgsAndRegex?.first ?: mutableListOf() } 75 private val fragRegex by lazy(LazyThreadSafetyMode.NONE) { fragArgsAndRegex?.second } 76 private val fragPattern by lazy { fragRegex?.let { Regex(it, RegexOption.IGNORE_CASE) } } 77 78 // mime 79 private var mimeTypeRegex: String? = null 80 private val mimeTypePattern by lazy { mimeTypeRegex?.let { Regex(it) } } 81 82 /** Arguments present in the deep link, including both path and query arguments. */ 83 internal val argumentsNames: List<String> 84 get() = pathArgs + queryArgsMap.values.flatMap { it.arguments } + fragArgs 85 86 public var isExactDeepLink: Boolean = false 87 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) get 88 internal set 89 90 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 91 public constructor(uri: String) : this(uri, null, null) 92 93 private fun buildRegex( 94 uri: String, 95 args: MutableList<String>, 96 uriRegex: StringBuilder, 97 ) { 98 var result = FILL_IN_PATTERN.find(uri) 99 var appendPos = 0 100 while (result != null) { 101 val argName = result.groups[1]!!.value 102 args.add(argName) 103 // Use Regex.escape() to treat the input string as a literal 104 if (result.range.first > appendPos) { 105 uriRegex.append(Regex.escape(uri.substring(appendPos, result.range.first))) 106 } 107 uriRegex.append(PATH_REGEX.pattern) 108 appendPos = result.range.last + 1 109 result = result.next() 110 } 111 if (appendPos < uri.length) { 112 // Use Regex.escape() to treat the input string as a literal 113 uriRegex.append(Regex.escape(uri.substring(appendPos))) 114 } 115 } 116 117 internal fun matches(uri: NavUri): Boolean { 118 return matches(NavDeepLinkRequest(uri, null, null)) 119 } 120 121 /** 122 * Checks for matches on uri, action, and mimType between a deepLink and a deepLinkRequest. 123 * 124 * Returns false if either 125 * 1. the deepLink's field is non-null while requested link's field is null 126 * 2. both fields are non-null but don't match 127 * 128 * Returns true otherwise (including when both fields are null). 129 */ 130 internal fun matches(deepLinkRequest: NavDeepLinkRequest): Boolean = 131 matchUri(deepLinkRequest.uri) && 132 matchAction(deepLinkRequest.action) && 133 matchMimeType(deepLinkRequest.mimeType) 134 135 private fun matchUri(uri: NavUri?): Boolean = 136 when { 137 pathPattern == null -> true 138 uri == null -> false 139 else -> pathPattern!!.matches(uri.toString()) 140 } 141 142 private fun matchAction(action: String?): Boolean = 143 when { 144 this.action == null -> true 145 action == null -> false 146 else -> this.action == action 147 } 148 149 private fun matchMimeType(mimeType: String?): Boolean = 150 when { 151 this.mimeType == null -> true 152 mimeType == null -> false 153 else -> mimeTypePattern!!.matches(mimeType) 154 } 155 156 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 157 public fun getMimeTypeMatchRating(mimeType: String): Int { 158 return if (this.mimeType == null || !mimeTypePattern!!.matches(mimeType)) { 159 -1 160 } else MimeType(this.mimeType).compareTo(MimeType(mimeType)) 161 } 162 163 @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "NullableCollection") 164 /** 165 * Pattern.compile has no nullability for the regex parameter 166 * 167 * May return null if any of the following: 168 * 1. missing required arguments that don't have default values 169 * 2. wrong value type (i.e. null for non-nullable arg) 170 * 3. other exceptions from parsing an argument value 171 * 172 * May return empty SavedState if any of the following: 173 * 1. deeplink has no arguments 174 * 2. deeplink contains arguments with unknown default values (i.e. deeplink from safe args with 175 * unknown default values) 176 */ 177 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 178 public fun getMatchingArguments( 179 deepLink: NavUri, 180 arguments: Map<String, NavArgument?> 181 ): SavedState? { 182 // first check overall uri pattern for quick return if general pattern does not match 183 val result = pathPattern?.matchEntire(deepLink.toString()) ?: return null 184 185 // get matching path and query arguments and store in bundle 186 val savedState = savedState() 187 if (!getMatchingPathArguments(result, savedState, arguments)) return null 188 if (isParameterizedQuery && !getMatchingQueryArguments(deepLink, savedState, arguments)) { 189 return null 190 } 191 // no match on optional fragment should not prevent a link from matching otherwise 192 getMatchingUriFragment(deepLink.getFragment(), savedState, arguments) 193 194 // Check that all required arguments are present in bundle 195 val missingRequiredArguments = 196 arguments.missingRequiredArguments { argName -> !savedState.read { contains(argName) } } 197 if (missingRequiredArguments.isNotEmpty()) return null 198 199 return savedState 200 } 201 202 /** 203 * Returns a SavedState containing matching path and query arguments with the requested uri. It 204 * returns empty SavedState if this Deeplink's path pattern does not match with the uri. 205 */ 206 internal fun getMatchingPathAndQueryArgs( 207 deepLink: NavUri?, 208 arguments: Map<String, NavArgument?> 209 ): SavedState { 210 val savedState = savedState() 211 if (deepLink == null) return savedState 212 val result = pathPattern?.matchEntire(deepLink.toString()) ?: return savedState 213 214 getMatchingPathArguments(result, savedState, arguments) 215 if (isParameterizedQuery) getMatchingQueryArguments(deepLink, savedState, arguments) 216 return savedState 217 } 218 219 private fun getMatchingUriFragment( 220 fragment: String?, 221 savedState: SavedState, 222 arguments: Map<String, NavArgument?> 223 ) { 224 // Base condition of a matching fragment is a complete match on regex pattern. If a 225 // required fragment arg is present while regex does not match, this will be caught later 226 // on as a non-match when we check for presence of required args in the bundle. 227 val result = fragPattern?.matchEntire(fragment.toString()) ?: return 228 229 this.fragArgs.mapIndexed { index, argumentName -> 230 val value = result.groups[index + 1]?.value?.let { NavUriUtils.decode(it) }.orEmpty() 231 val argument = arguments[argumentName] 232 try { 233 parseArgument(savedState, argumentName, value, argument) 234 } catch (e: IllegalArgumentException) { 235 // parse failed, quick return 236 return 237 } 238 } 239 } 240 241 private fun getMatchingPathArguments( 242 result: MatchResult, 243 savedState: SavedState, 244 arguments: Map<String, NavArgument?> 245 ): Boolean { 246 this.pathArgs.mapIndexed { index, argumentName -> 247 val value = result.groups[index + 1]?.value?.let { NavUriUtils.decode(it) }.orEmpty() 248 val argument = arguments[argumentName] 249 try { 250 parseArgument(savedState, argumentName, value, argument) 251 } catch (e: IllegalArgumentException) { 252 // Failed to parse means this isn't a valid deep link 253 // for the given URI - i.e., the URI contains a non-integer 254 // value for an integer argument 255 return false 256 } 257 } 258 // parse success 259 return true 260 } 261 262 private fun getMatchingQueryArguments( 263 deepLink: NavUri, 264 savedState: SavedState, 265 arguments: Map<String, NavArgument?> 266 ): Boolean { 267 // key is queryParameterName (argName could be different), value is NavDeepLink.ParamQuery 268 queryArgsMap.forEach { entry -> 269 val paramName = entry.key 270 val storedParam = entry.value 271 272 // a list of the arg values under this queryParameterName 273 // collection types (i.e. list, array) would potentially have listOf(arg1, arg2, arg3, 274 // etc..) 275 // non-collection types would usually have listOf(theArgValue) 276 var inputParams = deepLink.getQueryParameters(paramName) 277 if (isSingleQueryParamValueOnly) { 278 // If the deep link contains a single query param with no value, 279 // we will treat everything after the '?' as the input parameter 280 val argValue = deepLink.getQuery() 281 if (argValue != null && argValue != deepLink.toString()) { 282 inputParams = listOf(argValue) 283 } 284 } 285 val parseSuccess = parseInputParams(inputParams, storedParam, savedState, arguments) 286 if (!parseSuccess) return false 287 } 288 // parse success 289 return true 290 } 291 292 /** 293 * @param inputParams list of arg values under the same Uri.queryParameterName. For example: 294 * 1. sample route "...?myArg=1&myArg=2" inputParams = listOf("1", "2") 295 * 2. sample route "...?myArg=John_Doe" inputParams = listOf("John_Doe") 296 * 297 * @param storedParam the [ParamQuery] for a single Uri.queryParameter 298 */ 299 private fun parseInputParams( 300 inputParams: List<String>, 301 storedParam: ParamQuery, 302 savedState: SavedState, 303 arguments: Map<String, NavArgument?>, 304 ): Boolean { 305 val tempSavedState = savedState() 306 // try to start off by adding an empty bundle if there is no default value. 307 storedParam.arguments.forEach { argName -> 308 val argument = arguments[argName] 309 val navType = argument?.type 310 // for CollectionNavType, only fallback to empty collection if there isn't a default 311 // value 312 if (navType is CollectionNavType && !argument.isDefaultValuePresent) { 313 navType.put(tempSavedState, argName, navType.emptyCollection()) 314 } 315 } 316 inputParams.forEach { inputParam -> 317 val argMatchResult = storedParam.paramRegex?.let { Regex(it).matchEntire(inputParam) } 318 // check if this particular arg value matches the expected regex. 319 // for example, if the query was list of Int like "...?intId=1&intId=2&intId=abc", 320 // this would return false when matching "abc". 321 if (argMatchResult == null) { 322 return false 323 } 324 // iterate over each argName under the same queryParameterName 325 storedParam.arguments.mapIndexed { index, argName -> 326 // make sure we get the correct value for this particular argName 327 // i.e. if route is "...?myArg={firstName}_{lastName}" 328 // and the inputParam is "John_Doe" 329 // we need to map values to argName like this: 330 // [firstName to "John", lastName to "Doe"] 331 332 val value = argMatchResult.groups[index + 1]?.value.orEmpty() 333 val argument = arguments[argName] 334 335 try { 336 if (!tempSavedState.read { contains(argName) }) { 337 // Passing in a value the exact same as the placeholder will be treated the 338 // as if no value was passed (unless value is based on String), 339 // being replaced if it is optional or throwing an error if it is required. 340 parseArgument(tempSavedState, argName, value, argument) 341 } else { 342 parseArgumentForRepeatedParam(tempSavedState, argName, value, argument) 343 } 344 } catch (e: IllegalArgumentException) { 345 // Failed to parse means that at least one of the arguments that 346 // were supposed to fill in the query parameter was not valid. 347 // We will need to handle it here. Values that are not handled 348 // here will just be excluded from the argument bundle. 349 } 350 } 351 } 352 savedState.write { putAll(tempSavedState) } 353 // parse success 354 return true 355 } 356 357 internal fun calculateMatchingPathSegments(requestedLink: NavUri?): Int { 358 if (requestedLink == null || uriPattern == null) return 0 359 360 val requestedPathSegments = requestedLink.getPathSegments() 361 val uriPathSegments = NavUriUtils.parse(uriPattern).getPathSegments() 362 363 val matches = requestedPathSegments.intersect(uriPathSegments) 364 return matches.size 365 } 366 367 /** 368 * Parses [value] based on the NavArgument's NavType and stores the result inside the 369 * [savedState]. Throws if parse fails. 370 */ 371 private fun parseArgument( 372 savedState: SavedState, 373 name: String, 374 value: String, 375 argument: NavArgument? 376 ) { 377 if (argument != null) { 378 val type = argument.type 379 type.parseAndPut(savedState, name, value) 380 } else { 381 savedState.write { putString(name, value) } 382 } 383 } 384 385 /** 386 * Parses subsequent arg values under the same queryParameterName 387 * 388 * For example with route "...?myArg=one&myArg=two&myArg=three", [savedState] is expected to 389 * already contain bundleOf([name] to "one"), and this function will parse & put values "two" 390 * and "three" into the SavedState under the same [name]. 391 */ 392 private fun parseArgumentForRepeatedParam( 393 savedState: SavedState, 394 name: String, 395 value: String?, 396 argument: NavArgument? 397 ): Boolean { 398 if (!savedState.read { contains(name) }) { 399 return true 400 } 401 if (argument != null) { 402 val type = argument.type 403 val previousValue = type[savedState, name] 404 type.parseAndPut(savedState, name, value, previousValue) 405 } 406 return false 407 } 408 409 /** Used to maintain query parameters and the mArguments they match with. */ 410 private class ParamQuery { 411 var paramRegex: String? = null 412 // list of arg names under the same queryParamName, i.e. "...?name={first}_{last}" 413 // queryParamName = "name", arguments = ["first", "last"] 414 val arguments = mutableListOf<String>() 415 416 fun addArgumentName(name: String) { 417 arguments.add(name) 418 } 419 420 fun getArgumentName(index: Int): String { 421 return arguments[index] 422 } 423 424 fun size(): Int { 425 return arguments.size 426 } 427 } 428 429 private class MimeType(mimeType: String) : Comparable<MimeType> { 430 var type: String 431 var subType: String 432 433 override fun compareTo(other: MimeType): Int { 434 var result = 0 435 // matching just subtypes is 1 436 // matching just types is 2 437 // matching both is 3 438 if (type == other.type) { 439 result += 2 440 } 441 if (subType == other.subType) { 442 result++ 443 } 444 return result 445 } 446 447 init { 448 val typeAndSubType = mimeType.split("/".toRegex()).dropLastWhile { it.isEmpty() } 449 type = typeAndSubType[0] 450 subType = typeAndSubType[1] 451 } 452 } 453 454 override fun equals(other: Any?): Boolean { 455 if (other == null || other !is NavDeepLink) return false 456 return uriPattern == other.uriPattern && 457 action == other.action && 458 mimeType == other.mimeType 459 } 460 461 override fun hashCode(): Int { 462 var result = 0 463 result = 31 * result + uriPattern.hashCode() 464 result = 31 * result + action.hashCode() 465 result = 31 * result + mimeType.hashCode() 466 return result 467 } 468 469 /** A builder for constructing [NavDeepLink] instances. */ 470 public class Builder { 471 472 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public constructor() 473 474 private var uriPattern: String? = null 475 private var action: String? = null 476 private var mimeType: String? = null 477 478 /** 479 * Set the uri pattern for the [NavDeepLink]. 480 * 481 * @param uriPattern The uri pattern to add to the NavDeepLink 482 * @return This builder. 483 */ 484 public fun setUriPattern(uriPattern: String): Builder { 485 this.uriPattern = uriPattern 486 return this 487 } 488 489 /** 490 * Set the uri pattern for the [NavDeepLink]. 491 * 492 * Arguments extracted from destination [T] will be automatically appended to the base path 493 * provided in [basePath]. 494 * 495 * Arguments are appended based on property name and in the same order as their declaration 496 * order in [T]. They are appended as query parameters if the argument has either: 497 * 1. a default value 498 * 2. a [NavType] of [CollectionNavType] 499 * 500 * Otherwise, the argument will be appended as path parameters. The final uriPattern is 501 * generated by concatenating `uriPattern + path parameters + query parameters`. 502 * 503 * For example, the `name` property in this class does not meet either conditions and will 504 * be appended as a path param. 505 * 506 * ``` 507 * @Serializable 508 * class MyClass(val name: String) 509 * ``` 510 * 511 * Given a uriPattern of "www.example.com", the generated final uriPattern will be 512 * `www.example.com/{name}`. 513 * 514 * The `name` property in this class has a default value and will be appended as a query. 515 * 516 * ``` 517 * @Serializable 518 * class MyClass(val name: String = "default") 519 * ``` 520 * 521 * Given a uriPattern of "www.example.com", the final generated uriPattern will be 522 * `www.example.com?name={name}` 523 * 524 * The append order is based on their declaration order in [T] 525 * 526 * ``` 527 * @Serializable 528 * class MyClass(val name: String = "default", val id: Int, val code: Int) 529 * ``` 530 * 531 * Given a uriPattern of "www.example.com", the final generated uriPattern will be 532 * `www.example.com/{id}/{code}?name={name}`. In this example, `name` is appended first as a 533 * query param, then `id` and `code` respectively as path params. The final pattern is then 534 * concatenated with `uriPattern + path + query`. 535 * 536 * @param T The destination's route from KClass 537 * @param basePath The base uri path to append arguments onto 538 * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom 539 * [NavType]. May be empty if [T] does not use custom NavTypes. 540 * @return This builder. 541 */ 542 public inline fun <reified T : Any> setUriPattern( 543 basePath: String, 544 typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), 545 ): Builder = setUriPattern(T::class, basePath, typeMap) 546 547 /** 548 * Set the uri pattern for the [NavDeepLink]. 549 * 550 * Arguments extracted from destination [T] will be automatically appended to the base path 551 * provided in [basePath]. 552 * 553 * Arguments are appended based on property name and in the same order as their declaration 554 * order in [T]. They are appended as query parameters if the argument has either: 555 * 1. a default value 556 * 2. a [NavType] of [CollectionNavType] 557 * 558 * Otherwise, the argument will be appended as path parameters. The final uriPattern is 559 * generated by concatenating `uriPattern + path parameters + query parameters`. 560 * 561 * For example, the `name` property in this class does not meet either conditions and will 562 * be appended as a path param. 563 * 564 * ``` 565 * @Serializable 566 * class MyClass(val name: String) 567 * ``` 568 * 569 * Given a uriPattern of "www.example.com", the generated final uriPattern will be 570 * `www.example.com/{name}`. 571 * 572 * The `name` property in this class has a default value and will be appended as a query. 573 * 574 * ``` 575 * @Serializable 576 * class MyClass(val name: String = "default") 577 * ``` 578 * 579 * Given a uriPattern of "www.example.com", the final generated uriPattern will be 580 * `www.example.com?name={name}` 581 * 582 * The append order is based on their declaration order in [T] 583 * 584 * ``` 585 * @Serializable 586 * class MyClass(val name: String = "default", val id: Int, val code: Int) 587 * ``` 588 * 589 * Given a uriPattern of "www.example.com", the final generated uriPattern will be 590 * `www.example.com/{id}/{code}?name={name}`. In this example, `name` is appended first as a 591 * query param, then `id` and `code` respectively as path params. The final pattern is then 592 * concatenated with `uriPattern + path + query`. 593 * 594 * @param route The destination's route from KClass 595 * @param basePath The base uri path to append arguments onto 596 * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom 597 * [NavType]. May be empty if [T] does not use custom NavTypes. 598 * @return This builder. 599 */ 600 @OptIn(InternalSerializationApi::class) 601 @JvmOverloads 602 public fun <T : Any> setUriPattern( 603 route: KClass<T>, 604 basePath: String, 605 typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), 606 ): Builder { 607 this.uriPattern = route.serializer().generateRoutePattern(typeMap, basePath) 608 return this 609 } 610 611 /** 612 * Set the action for the [NavDeepLink]. 613 * 614 * @param action the intent action for the NavDeepLink 615 * @return This builder. 616 * @throws IllegalArgumentException if the action is empty. 617 */ 618 public fun setAction(action: String): Builder { 619 // if the action given at runtime is empty we should throw 620 require(action.isNotEmpty()) { "The NavDeepLink cannot have an empty action." } 621 this.action = action 622 return this 623 } 624 625 /** 626 * Set the mimeType for the [NavDeepLink]. 627 * 628 * @param mimeType the mimeType for the NavDeepLink 629 * @return This builder. 630 */ 631 public fun setMimeType(mimeType: String): Builder { 632 this.mimeType = mimeType 633 return this 634 } 635 636 /** 637 * Build the [NavDeepLink] specified by this builder. 638 * 639 * @return the newly constructed NavDeepLink. 640 */ 641 public fun build(): NavDeepLink { 642 return NavDeepLink(uriPattern, action, mimeType) 643 } 644 645 internal companion object { 646 /** 647 * Creates a [NavDeepLink.Builder] with a set uri pattern. 648 * 649 * @param uriPattern The uri pattern to add to the NavDeepLink 650 * @return a [Builder] instance 651 */ 652 @JvmStatic 653 fun fromUriPattern(uriPattern: String): Builder { 654 val builder = Builder() 655 builder.setUriPattern(uriPattern) 656 return builder 657 } 658 659 /** 660 * Creates a [NavDeepLink.Builder] with a set uri pattern. 661 * 662 * Arguments extracted from destination [T] will be automatically appended to the base 663 * path provided in [basePath] 664 * 665 * @param T The destination's route from KClass 666 * @param basePath The base uri path to append arguments onto 667 * @param typeMap map of destination arguments' kotlin type [KType] to its respective 668 * custom [NavType]. May be empty if [T] does not use custom NavTypes. 669 * @return a [Builder] instance 670 */ 671 @JvmStatic 672 inline fun <reified T : Any> fromUriPattern( 673 basePath: String, 674 typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(), 675 ): Builder { 676 val builder = Builder() 677 builder.setUriPattern(T::class, basePath, typeMap) 678 return builder 679 } 680 681 /** 682 * Creates a [NavDeepLink.Builder] with a set action. 683 * 684 * @param action the intent action for the NavDeepLink 685 * @return a [Builder] instance 686 * @throws IllegalArgumentException if the action is empty. 687 */ 688 @JvmStatic 689 fun fromAction(action: String): Builder { 690 // if the action given at runtime is empty we should throw 691 require(action.isNotEmpty()) { "The NavDeepLink cannot have an empty action." } 692 val builder = Builder() 693 builder.setAction(action) 694 return builder 695 } 696 697 /** 698 * Creates a [NavDeepLink.Builder] with a set mimeType. 699 * 700 * @param mimeType the mimeType for the NavDeepLink 701 * @return a [Builder] instance 702 */ 703 @JvmStatic 704 fun fromMimeType(mimeType: String): Builder { 705 val builder = Builder() 706 builder.setMimeType(mimeType) 707 return builder 708 } 709 } 710 } 711 712 private companion object { 713 private val SCHEME_PATTERN = Regex("^[a-zA-Z]+[+\\w\\-.]*:") 714 private val FILL_IN_PATTERN = Regex("\\{(.+?)\\}") 715 private val SCHEME_REGEX = Regex("http[s]?://") 716 private val WILDCARD_REGEX = Regex(".*") 717 // allows for empty path arguments i.e. empty strings "" 718 private val PATH_REGEX = Regex("([^/]*?|)") 719 private val QUERY_PATTERN = Regex("^[^?#]+\\?([^#]*).*") 720 721 // TODO: Use [RegexOption.DOT_MATCHES_ALL] once available in common 722 // https://youtrack.jetbrains.com/issue/KT-67574 723 private const val ANY_SYMBOLS_IN_THE_TAIL = "([\\s\\S]+?)?" 724 } 725 726 private fun parsePath() { 727 if (uriPattern == null) return 728 729 val uriRegex = StringBuilder("^") 730 // append scheme pattern 731 if (!SCHEME_PATTERN.containsMatchIn(uriPattern)) { 732 uriRegex.append(SCHEME_REGEX.pattern) 733 } 734 // extract beginning of uriPattern until it hits either a query(?), a framgment(#), or 735 // end of uriPattern 736 Regex("(\\?|#|$)").find(uriPattern)?.let { 737 buildRegex(uriPattern.substring(0, it.range.first), pathArgs, uriRegex) 738 isExactDeepLink = !uriRegex.contains(WILDCARD_REGEX) && !uriRegex.contains(PATH_REGEX) 739 // Match either the end of string if all params are optional or match the 740 // question mark (or pound symbol) and 0 or more characters after it 741 uriRegex.append("($|(\\?(.)*)|(#(.)*))") 742 } 743 // we need to specifically escape any .* instances to ensure 744 // they are still treated as wildcards in our final regex 745 pathRegex = uriRegex.toString().saveWildcardInRegex() 746 } 747 748 private fun parseQuery(): MutableMap<String, ParamQuery> { 749 val paramArgMap = mutableMapOf<String, ParamQuery>() 750 if (!isParameterizedQuery) return paramArgMap 751 val uri = NavUriUtils.parse(uriPattern!!) 752 753 for (paramName in uri.getQueryParameterNames()) { 754 val argRegex = StringBuilder() 755 val queryParams = uri.getQueryParameters(paramName) 756 require(queryParams.size <= 1) { 757 "Query parameter $paramName must only be present once in $uriPattern. " + 758 "To support repeated query parameters, use an array type for your " + 759 "argument and the pattern provided in your URI will be used to " + 760 "parse each query parameter instance." 761 } 762 // example of singleQueryParamValueOnly "www.example.com?{arg}" 763 val queryParam = 764 queryParams.firstOrNull() ?: paramName.apply { isSingleQueryParamValueOnly = true } 765 var result = FILL_IN_PATTERN.find(queryParam) 766 var appendPos = 0 767 val param = ParamQuery() 768 // Build the regex for each query param 769 while (result != null) { 770 // matcher.group(1) as String = "tab" (the extracted param arg from {tab}) 771 param.addArgumentName(result.groups[1]!!.value) 772 if (result.range.first > appendPos) { 773 val inputLiteral = queryParam.substring(appendPos, result.range.first) 774 argRegex.append(Regex.escape(inputLiteral)) 775 } 776 argRegex.append(ANY_SYMBOLS_IN_THE_TAIL) 777 appendPos = result.range.last + 1 778 result = result.next() 779 } 780 if (appendPos < queryParam.length) { 781 argRegex.append(Regex.escape(queryParam.substring(appendPos))) 782 } 783 argRegex.append("$") 784 785 // Save the regex with wildcards unquoted, and add the param to the map with its 786 // name as the key 787 param.paramRegex = argRegex.toString().saveWildcardInRegex() 788 paramArgMap[paramName] = param 789 } 790 return paramArgMap 791 } 792 793 private fun parseFragment(): Pair<MutableList<String>, String>? { 794 if (uriPattern == null || NavUriUtils.parse(uriPattern).getFragment() == null) return null 795 796 val fragArgs = mutableListOf<String>() 797 val fragment = NavUriUtils.parse(uriPattern).getFragment() 798 val fragRegex = StringBuilder() 799 buildRegex(fragment!!, fragArgs, fragRegex) 800 return fragArgs to fragRegex.toString() 801 } 802 803 private fun parseMime() { 804 if (mimeType == null) return 805 806 val mimeTypePattern = Regex("^[\\s\\S]+/[\\s\\S]+$") 807 require(mimeTypePattern.matches(mimeType)) { 808 "The given mimeType $mimeType does not match to required \"type/subtype\" format" 809 } 810 811 // get the type and subtype of the mimeType 812 val splitMimeType = MimeType(mimeType) 813 814 // the matching pattern can have the exact name or it can be wildcard literal (*) 815 val regex = "^(${splitMimeType.type}|[*]+)/(${splitMimeType.subType}|[*]+)$" 816 817 // if the deep link type or subtype is wildcard, allow anything 818 mimeTypeRegex = regex.replace("*|[*]", "[\\s\\S]") 819 } 820 821 // for more info see #Regexp.escape platform actuals 822 private fun String.saveWildcardInRegex(): String = 823 // non-js regex escaping 824 if (this.contains("\\Q") && this.contains("\\E")) replace(".*", "\\E.*\\Q") 825 // js regex escaping 826 else if (this.contains("\\.\\*")) replace("\\.\\*", ".*") 827 // fallback 828 else this 829 830 init { 831 parsePath() 832 parseMime() 833 } 834 } 835