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