Home | History | Annotate | Download | only in generator
      1 /*
      2  * Copyright 2018 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 
     17 package androidx.navigation.safe.args.generator
     18 
     19 import androidx.navigation.safe.args.generator.models.Action
     20 import androidx.navigation.safe.args.generator.models.Argument
     21 import androidx.navigation.safe.args.generator.models.Destination
     22 import androidx.navigation.safe.args.generator.models.ResReference
     23 import java.io.File
     24 import java.io.FileReader
     25 
     26 private const val TAG_NAVIGATION = "navigation"
     27 private const val TAG_ACTION = "action"
     28 private const val TAG_ARGUMENT = "argument"
     29 
     30 private const val ATTRIBUTE_ID = "id"
     31 private const val ATTRIBUTE_DESTINATION = "destination"
     32 private const val ATTRIBUTE_DEFAULT_VALUE = "defaultValue"
     33 private const val ATTRIBUTE_NAME = "name"
     34 private const val ATTRIBUTE_TYPE = "type"
     35 
     36 private const val NAMESPACE_RES_AUTO = "http://schemas.android.com/apk/res-auto"
     37 private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
     38 
     39 internal class NavParser(
     40     private val parser: XmlPositionParser,
     41     private val context: Context,
     42     private val rFilePackage: String,
     43     private val applicationId: String
     44 ) {
     45 
     46     companion object {
     47         fun parseNavigationFile(
     48             navigationXml: File,
     49             rFilePackage: String,
     50             applicationId: String,
     51             context: Context
     52         ): Destination {
     53             FileReader(navigationXml).use { reader ->
     54                 val parser = XmlPositionParser(navigationXml.path, reader, context.logger)
     55                 parser.traverseStartTags { true }
     56                 return NavParser(parser, context, rFilePackage, applicationId).parseDestination()
     57             }
     58         }
     59     }
     60 
     61     internal fun parseDestination(): Destination {
     62         val position = parser.xmlPosition()
     63         val type = parser.name()
     64         val name = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_NAME) ?: ""
     65         val idValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_ID)
     66         val args = mutableListOf<Argument>()
     67         val actions = mutableListOf<Action>()
     68         val nested = mutableListOf<Destination>()
     69         parser.traverseInnerStartTags {
     70             when {
     71                 parser.name() == TAG_ACTION -> actions.add(parseAction())
     72                 parser.name() == TAG_ARGUMENT -> args.add(parseArgument())
     73                 type == TAG_NAVIGATION -> nested.add(parseDestination())
     74             }
     75         }
     76 
     77         val id = idValue?.let { parseId(idValue, rFilePackage, position) }
     78         val className = Destination.createName(id, name, applicationId)
     79         if (className == null && (actions.isNotEmpty() || args.isNotEmpty())) {
     80             context.logger.error(NavParserErrors.UNNAMED_DESTINATION, position)
     81             return context.createStubDestination()
     82         }
     83 
     84         return Destination(id, className, type, args, actions, nested)
     85     }
     86 
     87     private fun parseArgument(): Argument {
     88         val xmlPosition = parser.xmlPosition()
     89         val name = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_NAME)
     90         val defaultValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_DEFAULT_VALUE)
     91         val typeString = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE)
     92         if (name == null) return context.createStubArg()
     93 
     94         if (typeString == null && defaultValue != null) {
     95             return inferArgument(name, defaultValue, rFilePackage)
     96         }
     97 
     98         val type = NavType.from(typeString)
     99         if (type == null) {
    100             context.logger.error(NavParserErrors.unknownType(typeString), xmlPosition)
    101             return context.createStubArg()
    102         }
    103 
    104         if (defaultValue == null) {
    105             return Argument(name, type, null)
    106         }
    107 
    108         val defaultTypedValue = when (type) {
    109             NavType.INT -> parseIntValue(defaultValue)
    110             NavType.FLOAT -> parseFloatValue(defaultValue)
    111             NavType.BOOLEAN -> parseBoolean(defaultValue)
    112             NavType.REFERENCE -> parseReference(defaultValue, rFilePackage)?.let {
    113                 ReferenceValue(it)
    114             }
    115             NavType.STRING -> StringValue(defaultValue)
    116         }
    117 
    118         if (defaultTypedValue == null) {
    119             val errorMessage = when (type) {
    120                 NavType.REFERENCE -> NavParserErrors.invalidDefaultValueReference(defaultValue)
    121                 else -> NavParserErrors.invalidDefaultValue(defaultValue, type)
    122             }
    123             context.logger.error(errorMessage, xmlPosition)
    124             return context.createStubArg()
    125         }
    126 
    127         return Argument(name, type, defaultTypedValue)
    128     }
    129 
    130     private fun parseAction(): Action {
    131         val idValue = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_ID)
    132         val destValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_DESTINATION)
    133         val args = mutableListOf<Argument>()
    134         val position = parser.xmlPosition()
    135         parser.traverseInnerStartTags {
    136             if (parser.name() == TAG_ARGUMENT) {
    137                 args.add(parseArgument())
    138             }
    139         }
    140 
    141         val id = if (idValue != null) {
    142             parseId(idValue, rFilePackage, position)
    143         } else {
    144             context.createStubId()
    145         }
    146         val destination = destValue?.let { parseId(destValue, rFilePackage, position) }
    147         return Action(id, destination, args)
    148     }
    149 
    150     private fun parseId(
    151         xmlId: String,
    152         rFilePackage: String,
    153         xmlPosition: XmlPosition
    154     ): ResReference {
    155         val ref = parseReference(xmlId, rFilePackage)
    156         if (ref?.isId() == true) {
    157             return ref
    158         }
    159         context.logger.error(NavParserErrors.invalidId(xmlId), xmlPosition)
    160         return context.createStubId()
    161     }
    162 }
    163 
    164 internal fun inferArgument(name: String, defaultValue: String, rFilePackage: String): Argument {
    165     val reference = parseReference(defaultValue, rFilePackage)
    166     if (reference != null) {
    167         return Argument(name, NavType.REFERENCE, ReferenceValue(reference))
    168     }
    169     val intValue = parseIntValue(defaultValue)
    170     if (intValue != null) {
    171         return Argument(name, NavType.INT, intValue)
    172     }
    173     val floatValue = parseFloatValue(defaultValue)
    174     if (floatValue != null) {
    175         return Argument(name, NavType.FLOAT, floatValue)
    176     }
    177     val boolValue = parseBoolean(defaultValue)
    178     if (boolValue != null) {
    179         return Argument(name, NavType.BOOLEAN, boolValue)
    180     }
    181     return Argument(name, NavType.STRING, StringValue(defaultValue))
    182 }
    183 
    184 // @[+][package:]id/resource_name -> package.R.id.resource_name
    185 private val RESOURCE_REGEX = Regex("^@[+]?(.+?:)?(.+?)/(.+)$")
    186 
    187 internal fun parseReference(xmlValue: String, rFilePackage: String): ResReference? {
    188     val matchEntire = RESOURCE_REGEX.matchEntire(xmlValue) ?: return null
    189     val groups = matchEntire.groupValues
    190     val resourceName = groups.last()
    191     val resType = groups[groups.size - 2]
    192     val packageName = if (groups[1].isNotEmpty()) groups[1].removeSuffix(":") else rFilePackage
    193     return ResReference(packageName, resType, resourceName)
    194 }
    195 
    196 internal fun parseIntValue(value: String): IntValue? {
    197     try {
    198         if (value.startsWith("0x")) {
    199             Integer.parseUnsignedInt(value.substring(2), 16)
    200         } else {
    201             Integer.parseInt(value)
    202         }
    203     } catch (ex: NumberFormatException) {
    204         return null
    205     }
    206     return IntValue(value)
    207 }
    208 
    209 private fun parseFloatValue(value: String): FloatValue? =
    210         value.toFloatOrNull()?.let { FloatValue(value) }
    211 
    212 private fun parseBoolean(value: String): BooleanValue? {
    213     if (value == "true" || value == "false") {
    214         return BooleanValue(value)
    215     }
    216     return null
    217 }
    218