1 /*
<lambda>null2  * 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.NavParserErrors.sameSanitizedNameActions
20 import androidx.navigation.safe.args.generator.NavParserErrors.sameSanitizedNameArguments
21 import androidx.navigation.safe.args.generator.ext.toCamelCase
22 import androidx.navigation.safe.args.generator.models.Action
23 import androidx.navigation.safe.args.generator.models.Argument
24 import androidx.navigation.safe.args.generator.models.Destination
25 import androidx.navigation.safe.args.generator.models.IncludedDestination
26 import androidx.navigation.safe.args.generator.models.ResReference
27 import java.io.File
28 import java.io.FileReader
29 
30 private const val TAG_NAVIGATION = "navigation"
31 private const val TAG_ACTION = "action"
32 private const val TAG_ARGUMENT = "argument"
33 private const val TAG_INCLUDE = "include"
34 
35 private const val ATTRIBUTE_ID = "id"
36 private const val ATTRIBUTE_DESTINATION = "destination"
37 private const val ATTRIBUTE_DEFAULT_VALUE = "defaultValue"
38 private const val ATTRIBUTE_NAME = "name"
39 private const val ATTRIBUTE_TYPE = "argType"
40 private const val ATTRIBUTE_TYPE_DEPRECATED = "type"
41 private const val ATTRIBUTE_NULLABLE = "nullable"
42 private const val ATTRIBUTE_GRAPH = "graph"
43 
44 const val VALUE_NULL = "@null"
45 private const val VALUE_TRUE = "true"
46 private const val VALUE_FALSE = "false"
47 
48 private const val NAMESPACE_RES_AUTO = "http://schemas.android.com/apk/res-auto"
49 private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
50 
51 internal class NavParser(
52     private val parser: XmlPositionParser,
53     private val context: Context,
54     private val rFilePackage: String,
55     private val applicationId: String
56 ) {
57 
58     companion object {
59         fun parseNavigationFile(
60             navigationXml: File,
61             rFilePackage: String,
62             applicationId: String,
63             context: Context
64         ): Destination {
65             FileReader(navigationXml).use { reader ->
66                 val parser = XmlPositionParser(navigationXml.path, reader, context.logger)
67                 parser.traverseStartTags { true }
68                 return NavParser(parser, context, rFilePackage, applicationId).parseDestination()
69             }
70         }
71     }
72 
73     internal fun parseDestination(): Destination {
74         val position = parser.xmlPosition()
75         val type = parser.name()
76         val name = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_NAME) ?: ""
77         val idValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_ID)
78         val args = mutableListOf<Argument>()
79         val actions = mutableListOf<Action>()
80         val nested = mutableListOf<Destination>()
81         val included = mutableListOf<IncludedDestination>()
82         parser.traverseInnerStartTags {
83             when {
84                 parser.name() == TAG_ACTION -> actions.add(parseAction())
85                 parser.name() == TAG_ARGUMENT -> args.add(parseArgument())
86                 parser.name() == TAG_INCLUDE -> included.add(parseIncludeDestination())
87                 type == TAG_NAVIGATION -> nested.add(parseDestination())
88             }
89         }
90 
91         actions
92             .groupBy { it.id.javaIdentifier.toCamelCase() }
93             .forEach { (sanitizedName, actions) ->
94                 if (actions.size > 1) {
95                     context.logger.error(sameSanitizedNameActions(sanitizedName, actions), position)
96                 }
97             }
98 
99         args
100             .groupBy { it.sanitizedName }
101             .forEach { (sanitizedName, args) ->
102                 if (args.size > 1) {
103                     context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
104                 }
105             }
106 
107         val id = idValue?.let { parseId(idValue, rFilePackage, position) }
108         val className = Destination.createName(id, name, applicationId)
109         if (className == null && (actions.isNotEmpty() || args.isNotEmpty())) {
110             context.logger.error(NavParserErrors.UNNAMED_DESTINATION, position)
111             return context.createStubDestination()
112         }
113 
114         return Destination(id, className, type, args, actions, nested, included)
115     }
116 
117     private fun parseIncludeDestination(): IncludedDestination {
118         val xmlPosition = parser.xmlPosition()
119 
120         val graphValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_GRAPH)
121         if (graphValue == null) {
122             context.logger.error(NavParserErrors.MISSING_GRAPH_ATTR, xmlPosition)
123             return context.createStubIncludedDestination()
124         }
125 
126         val graphRef = parseReference(graphValue, rFilePackage)
127         if (graphRef == null || graphRef.resType != "navigation") {
128             context.logger.error(NavParserErrors.invalidNavReference(graphValue), xmlPosition)
129             return context.createStubIncludedDestination()
130         }
131 
132         return IncludedDestination(graphRef)
133     }
134 
135     private fun parseArgument(): Argument {
136         val xmlPosition = parser.xmlPosition()
137         val name = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_NAME)
138         val defaultValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_DEFAULT_VALUE)
139         val typeString = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE)
140         val nullable =
141             parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_NULLABLE)?.let { it == VALUE_TRUE }
142                 ?: false
143 
144         if (name == null) return context.createStubArg()
145 
146         if (parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE_DEPRECATED) != null) {
147             context.logger.error(NavParserErrors.deprecatedTypeAttrUsed(name), xmlPosition)
148             return context.createStubArg()
149         }
150 
151         if (typeString == null && defaultValue != null) {
152             return inferArgument(name, defaultValue, rFilePackage)
153         }
154 
155         val type = NavType.from(typeString, rFilePackage)
156         if (nullable && !type.allowsNullable()) {
157             context.logger.error(NavParserErrors.typeIsNotNullable(typeString), xmlPosition)
158             return context.createStubArg()
159         }
160 
161         if (defaultValue == null) {
162             return Argument(name, type, null, nullable)
163         }
164 
165         val defaultTypedValue =
166             when (type) {
167                 IntType -> parseIntValue(defaultValue)
168                 LongType -> parseLongValue(defaultValue)
169                 FloatType -> parseFloatValue(defaultValue)
170                 BoolType -> parseBoolean(defaultValue)
171                 ReferenceType -> {
172                     when (defaultValue) {
173                         VALUE_NULL -> {
174                             context.logger.error(
175                                 NavParserErrors.nullDefaultValueReference(name),
176                                 xmlPosition
177                             )
178                             return context.createStubArg()
179                         }
180                         "0" -> IntValue("0")
181                         else ->
182                             parseReference(defaultValue, rFilePackage)?.let { ReferenceValue(it) }
183                     }
184                 }
185                 StringType -> {
186                     if (defaultValue == VALUE_NULL) {
187                         NullValue
188                     } else {
189                         StringValue(defaultValue)
190                     }
191                 }
192                 IntArrayType,
193                 LongArrayType,
194                 FloatArrayType,
195                 StringArrayType,
196                 BoolArrayType,
197                 ReferenceArrayType,
198                 is ObjectArrayType -> {
199                     if (defaultValue == VALUE_NULL) {
200                         NullValue
201                     } else {
202                         context.logger.error(
203                             NavParserErrors.defaultValueObjectType(typeString),
204                             xmlPosition
205                         )
206                         return context.createStubArg()
207                     }
208                 }
209                 is ObjectType -> {
210                     if (defaultValue == VALUE_NULL) {
211                         NullValue
212                     } else {
213                         EnumValue(type, defaultValue)
214                     }
215                 }
216                 else -> throw IllegalStateException("Unknown type: $type")
217             }
218 
219         if (defaultTypedValue == null) {
220             val errorMessage =
221                 when (type) {
222                     ReferenceType -> NavParserErrors.invalidDefaultValueReference(defaultValue)
223                     else -> NavParserErrors.invalidDefaultValue(defaultValue, type)
224                 }
225             context.logger.error(errorMessage, xmlPosition)
226             return context.createStubArg()
227         }
228 
229         if (!nullable && defaultTypedValue == NullValue) {
230             context.logger.error(NavParserErrors.defaultNullButNotNullable(name), xmlPosition)
231             return context.createStubArg()
232         }
233 
234         return Argument(name, type, defaultTypedValue, nullable)
235     }
236 
237     private fun parseAction(): Action {
238         val idValue = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_ID)
239         val destValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_DESTINATION)
240         val args = mutableListOf<Argument>()
241         val position = parser.xmlPosition()
242         parser.traverseInnerStartTags {
243             if (parser.name() == TAG_ARGUMENT) {
244                 args.add(parseArgument())
245             }
246         }
247 
248         args
249             .groupBy { it.sanitizedName }
250             .forEach { (sanitizedName, args) ->
251                 if (args.size > 1) {
252                     context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
253                 }
254             }
255 
256         val id =
257             if (idValue != null) {
258                 parseId(idValue, rFilePackage, position)
259             } else {
260                 context.createStubId()
261             }
262         val destination = destValue?.let { parseId(destValue, rFilePackage, position) }
263         return Action(id, destination, args)
264     }
265 
266     private fun parseId(
267         xmlId: String,
268         rFilePackage: String,
269         xmlPosition: XmlPosition
270     ): ResReference {
271         val ref = parseReference(xmlId, rFilePackage)
272         if (ref?.isId() == true) {
273             return ref
274         }
275         context.logger.error(NavParserErrors.invalidId(xmlId), xmlPosition)
276         return context.createStubId()
277     }
278 }
279 
inferArgumentnull280 internal fun inferArgument(name: String, defaultValue: String, rFilePackage: String): Argument {
281     val reference = parseReference(defaultValue, rFilePackage)
282     if (reference != null) {
283         val type =
284             when (reference.resType) {
285                 "color",
286                 "dimen",
287                 "integer" -> IntType
288                 "bool" -> BoolType
289                 "string" -> StringType
290                 else -> ReferenceType
291             }
292         return Argument(name, type, ReferenceValue(reference))
293     }
294     val longValue = parseLongValue(defaultValue)
295     if (longValue != null) {
296         return Argument(name, LongType, longValue)
297     }
298     val intValue = parseIntValue(defaultValue)
299     if (intValue != null) {
300         return Argument(name, IntType, intValue)
301     }
302     val floatValue = parseFloatValue(defaultValue)
303     if (floatValue != null) {
304         return Argument(name, FloatType, floatValue)
305     }
306     val boolValue = parseBoolean(defaultValue)
307     if (boolValue != null) {
308         return Argument(name, BoolType, boolValue)
309     }
310     return if (defaultValue == VALUE_NULL) {
311         Argument(name, StringType, NullValue, true)
312     } else {
313         Argument(name, StringType, StringValue(defaultValue))
314     }
315 }
316 
317 // @[+][package:]id/resource_name -> package.R.id.resource_name
318 private val RESOURCE_REGEX = Regex("^@[+]?(.+?:)?(.+?)/(.+)$")
319 
parseReferencenull320 internal fun parseReference(xmlValue: String, rFilePackage: String): ResReference? {
321     val matchEntire = RESOURCE_REGEX.matchEntire(xmlValue) ?: return null
322     val groups = matchEntire.groupValues
323     val resourceName = groups.last()
324     val resType = groups[groups.size - 2]
325     val packageName = if (groups[1].isNotEmpty()) groups[1].removeSuffix(":") else rFilePackage
326     return ResReference(packageName, resType, resourceName)
327 }
328 
parseIntValuenull329 internal fun parseIntValue(value: String): IntValue? {
330     try {
331         if (value.startsWith("0x")) {
332             Integer.parseUnsignedInt(value.substring(2), 16)
333         } else {
334             Integer.parseInt(value)
335         }
336     } catch (ex: NumberFormatException) {
337         return null
338     }
339     return IntValue(value)
340 }
341 
parseLongValuenull342 internal fun parseLongValue(value: String): LongValue? {
343     if (!value.endsWith('L')) {
344         return null
345     }
346     try {
347         val normalizedValue = value.substringBeforeLast('L')
348         if (normalizedValue.startsWith("0x")) {
349             normalizedValue.substring(2).toLong(16)
350         } else {
351             normalizedValue.toLong()
352         }
353     } catch (ex: NumberFormatException) {
354         return null
355     }
356     return LongValue(value)
357 }
358 
parseFloatValuenull359 private fun parseFloatValue(value: String): FloatValue? =
360     value.toFloatOrNull()?.let { FloatValue(value) }
361 
parseBooleannull362 private fun parseBoolean(value: String): BooleanValue? {
363     if (value == VALUE_TRUE || value == VALUE_FALSE) {
364         return BooleanValue(value)
365     }
366     return null
367 }
368