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