NavDeepLink.kt

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.navigation

import android.net.Uri
import android.os.Bundle
import androidx.annotation.RestrictTo
import java.util.regex.Matcher
import java.util.regex.Pattern

/**
 * NavDeepLink encapsulates the parsing and matching of a navigation deep link.
 *
 * This should be added to a [NavDestination] using
 * [NavDestination.addDeepLink].
 */
public class NavDeepLink internal constructor(
    /**
     * The uri pattern from the NavDeepLink.
     *
     * @see NavDeepLinkRequest.uri
     */
    public val uriPattern: String?,
    /**
     * The action from the NavDeepLink.
     *
     * @see NavDeepLinkRequest.action
     */
    public val action: String?,
    /**
     * The mimeType from the NavDeepLink.
     *
     * @see NavDeepLinkRequest.mimeType
     */
    public val mimeType: String?
) {
    private val pathArgs = mutableListOf<String>()
    private val paramArgMap = mutableMapOf<String, ParamQuery>()
    private val fragArgs = mutableListOf<String>()

    private var patternFinalRegex: String? = null
    private val pattern by lazy {
        patternFinalRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
    }
    private var isParameterizedQuery = false
    private var isSingleQueryParamValueOnly = false

    private var fragmentFinalRegex: String? = null
    private val fragmentPattern by lazy {
        fragmentFinalRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
    }

    private var mimeTypeFinalRegex: String? = null
    private val mimeTypePattern by lazy {
        mimeTypeFinalRegex?.let { Pattern.compile(it) }
    }

    /** Arguments present in the deep link, including both path and query arguments. */
    internal val argumentsNames: List<String>
        get() = pathArgs + paramArgMap.values.flatMap { it.arguments } + fragArgs

    public var isExactDeepLink: Boolean = false
        /** @suppress */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        get
        internal set

    /** @suppress */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public constructor(uri: String) : this(uri, null, null)

    private fun buildRegex(
        uri: String,
        args: MutableList<String>,
        uriRegex: StringBuilder,
    ) {
        val matcher = FILL_IN_PATTERN.matcher(uri)
        var appendPos = 0
        while (matcher.find()) {
            val argName = matcher.group(1) as String
            args.add(argName)
            // Use Pattern.quote() to treat the input string as a literal
            if (matcher.start() > appendPos) {
                uriRegex.append(Pattern.quote(uri.substring(appendPos, matcher.start())))
            }
            uriRegex.append("([^/]+?)")
            appendPos = matcher.end()
        }
        if (appendPos < uri.length) {
            // Use Pattern.quote() to treat the input string as a literal
            uriRegex.append(Pattern.quote(uri.substring(appendPos)))
        }
    }

    internal fun matches(uri: Uri): Boolean {
        return matches(NavDeepLinkRequest(uri, null, null))
    }

    internal fun matches(deepLinkRequest: NavDeepLinkRequest): Boolean {
        if (!matchUri(deepLinkRequest.uri)) {
            return false
        }
        return if (!matchAction(deepLinkRequest.action)) {
            false
        } else matchMimeType(deepLinkRequest.mimeType)
    }

    private fun matchUri(uri: Uri?): Boolean {
        // If the null status of both are not the same return false.
        return if (uri == null == (pattern != null)) {
            false
        } else uri == null || pattern!!.matcher(uri.toString()).matches()
        // If both are null return true, otherwise see if they match
    }

    private fun matchAction(action: String?): Boolean {
        // If the null status of both are not the same return false.
        return if (action == null == (this.action != null)) {
            false
        } else action == null || this.action == action
        // If both are null return true, otherwise see if they match
    }

    private fun matchMimeType(mimeType: String?): Boolean {
        // If the null status of both are not the same return false.
        return if (mimeType == null == (this.mimeType != null)) {
            false
        } else mimeType == null || mimeTypePattern!!.matcher(mimeType).matches()

        // If both are null return true, otherwise see if they match
    }

    /** @suppress */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun getMimeTypeMatchRating(mimeType: String): Int {
        return if (this.mimeType == null || !mimeTypePattern!!.matcher(mimeType).matches()) {
            -1
        } else MimeType(this.mimeType)
            .compareTo(MimeType(mimeType))
    }

    @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "NullableCollection")
    /** Pattern.compile has no nullability for the regex parameter
     * @suppress
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun getMatchingArguments(
        deepLink: Uri,
        arguments: Map<String, NavArgument?>
    ): Bundle? {
        // first check overall uri pattern for quick return if general pattern does not match
        val matcher = pattern?.matcher(deepLink.toString()) ?: return null
        if (!matcher.matches()) {
            return null
        }
        // get matching path and query arguments and store in bundle
        val bundle = Bundle()
        if (!getMatchingPathArguments(matcher, bundle, arguments)) return null
        if (isParameterizedQuery && !getMatchingQueryArguments(deepLink, bundle, arguments)) {
            return null
        }
        // no match on optional fragment should not prevent a link from matching otherwise
        getMatchingUriFragment(deepLink.fragment, bundle, arguments)

        // Check that all required arguments are present in bundle
        for ((argName, argument) in arguments.entries) {
            val argumentIsRequired = argument != null && !argument.isNullable &&
                !argument.isDefaultValuePresent
            if (argumentIsRequired && !bundle.containsKey(argName)) return null
        }

        return bundle
    }

    private fun getMatchingUriFragment(
        fragment: String?,
        bundle: Bundle,
        arguments: Map<String, NavArgument?>
    ) {
        // Base condition of a matching fragment is a complete match on regex pattern. If a
        // required fragment arg is present while regex does not match, this will be caught later
        // on as a non-match when we check for presence of required args in the bundle.
        val matcher = fragmentPattern?.matcher(fragment.toString()) ?: return
        if (!matcher.matches()) return

        this.fragArgs.mapIndexed { index, argumentName ->
            val value = Uri.decode(matcher.group(index + 1))
            val argument = arguments[argumentName]
            try {
                if (parseArgument(bundle, argumentName, value, argument)) {
                    return
                }
            } catch (e: IllegalArgumentException) {
                return
            }
        }
    }

    private fun getMatchingPathArguments(
        matcher: Matcher,
        bundle: Bundle,
        arguments: Map<String, NavArgument?>
    ): Boolean {
        this.pathArgs.mapIndexed { index, argumentName ->
            val value = Uri.decode(matcher.group(index + 1))
            val argument = arguments[argumentName]
            try {
                if (parseArgument(bundle, argumentName, value, argument)) {
                    return false
                }
            } catch (e: IllegalArgumentException) {
                // Failed to parse means this isn't a valid deep link
                // for the given URI - i.e., the URI contains a non-integer
                // value for an integer argument
                return false
            }
        }
        return true
    }

    private fun getMatchingQueryArguments(
        deepLink: Uri,
        bundle: Bundle,
        arguments: Map<String, NavArgument?>
    ): Boolean {
        paramArgMap.forEach { entry ->
            val paramName = entry.key
            val storedParam = entry.value

            var inputParams = deepLink.getQueryParameters(paramName)
            if (isSingleQueryParamValueOnly) {
                // If the deep link contains a single query param with no value,
                // we will treat everything after the '?' as the input parameter
                val argValue = deepLink.query
                if (argValue != deepLink.toString()) {
                    inputParams = listOf(argValue)
                }
            }
            if (!parseInputParams(inputParams, storedParam, bundle, arguments)) {
                return false
            }
        }
        return true
    }

    private fun parseInputParams(
        inputParams: List<String>?,
        storedParam: ParamQuery,
        bundle: Bundle,
        arguments: Map<String, NavArgument?>,
    ): Boolean {
        inputParams?.forEach { inputParam ->
            val argMatcher = storedParam.paramRegex?.let {
                Pattern.compile(
                    it, Pattern.DOTALL
                ).matcher(inputParam)
            }
            if (argMatcher == null || !argMatcher.matches()) {
                return false
            }

            val queryParamBundle = Bundle()
            try {
                storedParam.arguments.mapIndexed { index, argName ->
                    val value = argMatcher.group(index + 1) ?: ""
                    val argument = arguments[argName]
                    if (parseArgumentForRepeatedParam(bundle, argName, value, argument)) {
                        // Passing in a value the exact same as the placeholder will be treated the
                        // as if no value was passed, being replaced if it is optional or throwing an
                        // error if it is required.
                        if (value != "{$argName}" &&
                            parseArgument(queryParamBundle, argName, value, argument)
                        ) {
                            return false
                        }
                    }
                }
                bundle.putAll(queryParamBundle)
            } catch (e: IllegalArgumentException) {
                // Failed to parse means that at least one of the arguments that were supposed
                // to fill in the query parameter was not valid and therefore, we will exclude
                // that particular parameter from the argument bundle.
            }
        }
        return true
    }

    internal fun calculateMatchingPathSegments(requestedLink: Uri?): Int {
        if (requestedLink == null || uriPattern == null) return 0

        val requestedPathSegments = requestedLink.pathSegments
        val uriPathSegments = Uri.parse(uriPattern).pathSegments

        val matches = requestedPathSegments.intersect(uriPathSegments)
        return matches.size
    }

    private fun parseArgument(
        bundle: Bundle,
        name: String,
        value: String,
        argument: NavArgument?
    ): Boolean {
        if (argument != null) {
            val type = argument.type
            type.parseAndPut(bundle, name, value)
        } else {
            bundle.putString(name, value)
        }
        return false
    }

    private fun parseArgumentForRepeatedParam(
        bundle: Bundle,
        name: String,
        value: String?,
        argument: NavArgument?
    ): Boolean {
        if (!bundle.containsKey(name)) {
            return true
        }
        if (argument != null) {
            val type = argument.type
            val previousValue = type[bundle, name]
            type.parseAndPut(bundle, name, value, previousValue)
        }
        return false
    }

    /**
     * Used to maintain query parameters and the mArguments they match with.
     */
    private class ParamQuery {
        var paramRegex: String? = null
        val arguments = mutableListOf<String>()

        fun addArgumentName(name: String) {
            arguments.add(name)
        }

        fun getArgumentName(index: Int): String {
            return arguments[index]
        }

        fun size(): Int {
            return arguments.size
        }
    }

    private class MimeType(mimeType: String) : Comparable<MimeType> {
        var type: String
        var subType: String
        override fun compareTo(other: MimeType): Int {
            var result = 0
            // matching just subtypes is 1
            // matching just types is 2
            // matching both is 3
            if (type == other.type) {
                result += 2
            }
            if (subType == other.subType) {
                result++
            }
            return result
        }

        init {
            val typeAndSubType =
                mimeType.split("/".toRegex()).dropLastWhile { it.isEmpty() }
            type = typeAndSubType[0]
            subType = typeAndSubType[1]
        }
    }

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is NavDeepLink) return false
        return uriPattern == other.uriPattern &&
            action == other.action &&
            mimeType == other.mimeType
    }

    override fun hashCode(): Int {
        var result = 0
        result = 31 * result + uriPattern.hashCode()
        result = 31 * result + action.hashCode()
        result = 31 * result + mimeType.hashCode()
        return result
    }

    /**
     * A builder for constructing [NavDeepLink] instances.
     */
    public class Builder {

        /** @suppress */
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        public constructor()

        private var uriPattern: String? = null
        private var action: String? = null
        private var mimeType: String? = null

        /**
         * Set the uri pattern for the [NavDeepLink].
         *
         * @param uriPattern The uri pattern to add to the NavDeepLink
         *
         * @return This builder.
         */
        public fun setUriPattern(uriPattern: String): Builder {
            this.uriPattern = uriPattern
            return this
        }

        /**
         * Set the action for the [NavDeepLink].
         *
         * @throws IllegalArgumentException if the action is empty.
         *
         * @param action the intent action for the NavDeepLink
         *
         * @return This builder.
         */
        public fun setAction(action: String): Builder {
            // if the action given at runtime is empty we should throw
            require(action.isNotEmpty()) { "The NavDeepLink cannot have an empty action." }
            this.action = action
            return this
        }

        /**
         * Set the mimeType for the [NavDeepLink].
         *
         * @param mimeType the mimeType for the NavDeepLink
         *
         * @return This builder.
         */
        public fun setMimeType(mimeType: String): Builder {
            this.mimeType = mimeType
            return this
        }

        /**
         * Build the [NavDeepLink] specified by this builder.
         *
         * @return the newly constructed NavDeepLink.
         */
        public fun build(): NavDeepLink {
            return NavDeepLink(uriPattern, action, mimeType)
        }

        internal companion object {
            /**
             * Creates a [NavDeepLink.Builder] with a set uri pattern.
             *
             * @param uriPattern The uri pattern to add to the NavDeepLink
             * @return a [Builder] instance
             */
            @JvmStatic
            fun fromUriPattern(uriPattern: String): Builder {
                val builder = Builder()
                builder.setUriPattern(uriPattern)
                return builder
            }

            /**
             * Creates a [NavDeepLink.Builder] with a set action.
             *
             * @throws IllegalArgumentException if the action is empty.
             *
             * @param action the intent action for the NavDeepLink
             * @return a [Builder] instance
             */
            @JvmStatic
            fun fromAction(action: String): Builder {
                // if the action given at runtime is empty we should throw
                require(action.isNotEmpty()) { "The NavDeepLink cannot have an empty action." }
                val builder = Builder()
                builder.setAction(action)
                return builder
            }

            /**
             * Creates a [NavDeepLink.Builder] with a set mimeType.
             *
             * @param mimeType the mimeType for the NavDeepLink
             * @return a [Builder] instance
             */
            @JvmStatic
            fun fromMimeType(mimeType: String): Builder {
                val builder = Builder()
                builder.setMimeType(mimeType)
                return builder
            }
        }
    }

    private companion object {
        private val SCHEME_PATTERN = Pattern.compile("^[a-zA-Z]+[+\w\-.]*:")
        private val FILL_IN_PATTERN = Pattern.compile("\{(.+?)\}")
    }

    private fun parsePath() {
        if (uriPattern == null) return

        val uriRegex = StringBuilder("^")
        // append scheme pattern
        if (!SCHEME_PATTERN.matcher(uriPattern).find()) {
            uriRegex.append("http[s]?://")
        }
        // extract beginning of uriPattern until it hits either a query(?), a framgment(#), or
        // end of uriPattern
        var matcher = Pattern.compile("(\?|\#|$)").matcher(uriPattern)
        matcher.find().let {
            buildRegex(uriPattern.substring(0, matcher.start()), pathArgs, uriRegex)
            isExactDeepLink = !uriRegex.contains(".*") && !uriRegex.contains("([^/]+?)")
            // Match either the end of string if all params are optional or match the
            // question mark (or pound symbol) and 0 or more characters after it
            uriRegex.append("($|(\?(.)*)|(\#(.)*))")
        }
        // we need to specifically escape any .* instances to ensure
        // they are still treated as wildcards in our final regex
        patternFinalRegex = uriRegex.toString().replace(".*", "\E.*\Q")
    }

    private fun parseQuery() {
        if (uriPattern == null || Uri.parse(uriPattern).query == null) return
        val uri = Uri.parse(uriPattern)

        isParameterizedQuery = true

        for (paramName in uri.queryParameterNames) {
            val argRegex = StringBuilder()
            val queryParams = uri.getQueryParameters(paramName)
            require(queryParams.size <= 1) {
                "Query parameter $paramName must only be present once in $uriPattern." +
                    "To support repeated query parameters, use an array type for your" +
                    "argument and the pattern provided in your URI will be used to" +
                    "parse each query parameter instance."
            }
            val queryParam = queryParams.firstOrNull()
                ?: paramName.apply { isSingleQueryParamValueOnly = true }
            val matcher = FILL_IN_PATTERN.matcher(queryParam)
            var appendPos = 0
            val param = ParamQuery()
            // Build the regex for each query param
            while (matcher.find()) {
                // matcher.group(1) as String = "tab" (the extracted param arg from {tab})
                param.addArgumentName(matcher.group(1) as String)
                argRegex.append(
                    Pattern.quote(
                        queryParam.substring(
                            appendPos,
                            matcher.start()
                        )
                    )
                )
                argRegex.append("(.+?)?")
                appendPos = matcher.end()
            }
            if (appendPos < queryParam.length) {
                argRegex.append(Pattern.quote(queryParam.substring(appendPos)))
            }

            // Save the regex with wildcards unquoted, and add the param to the map with its
            // name as the key
            param.paramRegex = argRegex.toString().replace(".*", "\E.*\Q")
            paramArgMap[paramName] = param
        }
    }

    private fun parseFragment() {
        if (uriPattern == null || Uri.parse(uriPattern).fragment == null) return

        val fragment = Uri.parse(uriPattern).fragment
        val fragRegex = StringBuilder()
        buildRegex(fragment!!, fragArgs, fragRegex)
        fragmentFinalRegex = fragRegex.toString()
    }

    private fun parseMime() {
        if (mimeType == null) return

        val mimeTypePattern = Pattern.compile("^[\s\S]+/[\s\S]+$")
        val mimeTypeMatcher = mimeTypePattern.matcher(mimeType)
        require(mimeTypeMatcher.matches()) {
            "The given mimeType $mimeType does not match to required \"type/subtype\" format"
        }

        // get the type and subtype of the mimeType
        val splitMimeType = MimeType(
            mimeType
        )

        // the matching pattern can have the exact name or it can be wildcard literal (*)
        val mimeTypeRegex = "^(${splitMimeType.type}|[*]+)/(${splitMimeType.subType}|[*]+)$"
        println("cfok inside mimeTypeRegex $mimeTypeRegex")

        // if the deep link type or subtype is wildcard, allow anything
        mimeTypeFinalRegex = mimeTypeRegex.replace("*|[*]", "[\s\S]")
    }

    init {
        parsePath()
        parseQuery()
        parseFragment()
        parseMime()
    }
}