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