/*
* Copyright 2018 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.safe.args.generator
import androidx.navigation.safe.args.generator.NavParserErrors.sameSanitizedNameActions
import androidx.navigation.safe.args.generator.NavParserErrors.sameSanitizedNameArguments
import androidx.navigation.safe.args.generator.ext.toCamelCase
import androidx.navigation.safe.args.generator.models.Action
import androidx.navigation.safe.args.generator.models.Argument
import androidx.navigation.safe.args.generator.models.Destination
import androidx.navigation.safe.args.generator.models.IncludedDestination
import androidx.navigation.safe.args.generator.models.ResReference
import java.io.File
import java.io.FileReader
private const val TAG_NAVIGATION = "navigation"
private const val TAG_ACTION = "action"
private const val TAG_ARGUMENT = "argument"
private const val TAG_INCLUDE = "include"
private const val ATTRIBUTE_ID = "id"
private const val ATTRIBUTE_DESTINATION = "destination"
private const val ATTRIBUTE_DEFAULT_VALUE = "defaultValue"
private const val ATTRIBUTE_NAME = "name"
private const val ATTRIBUTE_TYPE = "argType"
private const val ATTRIBUTE_TYPE_DEPRECATED = "type"
private const val ATTRIBUTE_NULLABLE = "nullable"
private const val ATTRIBUTE_GRAPH = "graph"
const val VALUE_NULL = "@null"
private const val VALUE_TRUE = "true"
private const val VALUE_FALSE = "false"
private const val NAMESPACE_RES_AUTO = "http://schemas.android.com/apk/res-auto"
private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
internal class NavParser(
private val parser: XmlPositionParser,
private val context: Context,
private val rFilePackage: String,
private val applicationId: String
) {
companion object {
fun parseNavigationFile(
navigationXml: File,
rFilePackage: String,
applicationId: String,
context: Context
): Destination {
FileReader(navigationXml).use { reader ->
val parser = XmlPositionParser(navigationXml.path, reader, context.logger)
parser.traverseStartTags { true }
return NavParser(parser, context, rFilePackage, applicationId).parseDestination()
}
}
}
internal fun parseDestination(): Destination {
val position = parser.xmlPosition()
val type = parser.name()
val name = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_NAME) ?: ""
val idValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_ID)
val args = mutableListOf<Argument>()
val actions = mutableListOf<Action>()
val nested = mutableListOf<Destination>()
val included = mutableListOf<IncludedDestination>()
parser.traverseInnerStartTags {
when {
parser.name() == TAG_ACTION -> actions.add(parseAction())
parser.name() == TAG_ARGUMENT -> args.add(parseArgument())
parser.name() == TAG_INCLUDE -> included.add(parseIncludeDestination())
type == TAG_NAVIGATION -> nested.add(parseDestination())
}
}
actions.groupBy { it.id.javaIdentifier.toCamelCase() }.forEach { (sanitizedName, actions) ->
if (actions.size > 1) {
context.logger.error(sameSanitizedNameActions(sanitizedName, actions), position)
}
}
args.groupBy { it.sanitizedName }.forEach { (sanitizedName, args) ->
if (args.size > 1) {
context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
}
}
val id = idValue?.let { parseId(idValue, rFilePackage, position) }
val className = Destination.createName(id, name, applicationId)
if (className == null && (actions.isNotEmpty() || args.isNotEmpty())) {
context.logger.error(NavParserErrors.UNNAMED_DESTINATION, position)
return context.createStubDestination()
}
return Destination(id, className, type, args, actions, nested, included)
}
private fun parseIncludeDestination(): IncludedDestination {
val xmlPosition = parser.xmlPosition()
val graphValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_GRAPH)
if (graphValue == null) {
context.logger.error(NavParserErrors.MISSING_GRAPH_ATTR, xmlPosition)
return context.createStubIncludedDestination()
}
val graphRef = parseReference(graphValue, rFilePackage)
if (graphRef == null || graphRef.resType != "navigation") {
context.logger.error(NavParserErrors.invalidNavReference(graphValue), xmlPosition)
return context.createStubIncludedDestination()
}
return IncludedDestination(graphRef)
}
private fun parseArgument(): Argument {
val xmlPosition = parser.xmlPosition()
val name = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_NAME)
val defaultValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_DEFAULT_VALUE)
val typeString = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE)
val nullable = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_NULLABLE)?.let {
it == VALUE_TRUE
} ?: false
if (name == null) return context.createStubArg()
if (parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE_DEPRECATED) != null) {
context.logger.error(NavParserErrors.deprecatedTypeAttrUsed(name), xmlPosition)
return context.createStubArg()
}
if (typeString == null && defaultValue != null) {
return inferArgument(name, defaultValue, rFilePackage)
}
val type = NavType.from(typeString, rFilePackage)
if (nullable && !type.allowsNullable()) {
context.logger.error(NavParserErrors.typeIsNotNullable(typeString), xmlPosition)
return context.createStubArg()
}
if (defaultValue == null) {
return Argument(name, type, null, nullable)
}
val defaultTypedValue = when (type) {
IntType -> parseIntValue(defaultValue)
LongType -> parseLongValue(defaultValue)
FloatType -> parseFloatValue(defaultValue)
BoolType -> parseBoolean(defaultValue)
ReferenceType -> {
when (defaultValue) {
VALUE_NULL -> {
context.logger.error(NavParserErrors.nullDefaultValueReference(name),
xmlPosition)
return context.createStubArg()
}
"0" -> IntValue("0")
else -> parseReference(defaultValue, rFilePackage)?.let {
ReferenceValue(it)
}
}
}
StringType -> {
if (defaultValue == VALUE_NULL) {
NullValue
} else {
StringValue(defaultValue)
}
}
IntArrayType, LongArrayType, FloatArrayType, StringArrayType,
BoolArrayType, ReferenceArrayType, is ObjectArrayType -> {
if (defaultValue == VALUE_NULL) {
NullValue
} else {
context.logger.error(
NavParserErrors.defaultValueObjectType(typeString),
xmlPosition
)
return context.createStubArg()
}
}
is ObjectType -> {
if (defaultValue == VALUE_NULL) {
NullValue
} else {
EnumValue(type, defaultValue)
}
}
else -> throw IllegalStateException("Unknown type: $type")
}
if (defaultTypedValue == null) {
val errorMessage = when (type) {
ReferenceType -> NavParserErrors.invalidDefaultValueReference(defaultValue)
else -> NavParserErrors.invalidDefaultValue(defaultValue, type)
}
context.logger.error(errorMessage, xmlPosition)
return context.createStubArg()
}
if (!nullable && defaultTypedValue == NullValue) {
context.logger.error(NavParserErrors.defaultNullButNotNullable(name), xmlPosition)
return context.createStubArg()
}
return Argument(name, type, defaultTypedValue, nullable)
}
private fun parseAction(): Action {
val idValue = parser.attrValueOrError(NAMESPACE_ANDROID, ATTRIBUTE_ID)
val destValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_DESTINATION)
val args = mutableListOf<Argument>()
val position = parser.xmlPosition()
parser.traverseInnerStartTags {
if (parser.name() == TAG_ARGUMENT) {
args.add(parseArgument())
}
}
args.groupBy { it.sanitizedName }.forEach { (sanitizedName, args) ->
if (args.size > 1) {
context.logger.error(sameSanitizedNameArguments(sanitizedName, args), position)
}
}
val id = if (idValue != null) {
parseId(idValue, rFilePackage, position)
} else {
context.createStubId()
}
val destination = destValue?.let { parseId(destValue, rFilePackage, position) }
return Action(id, destination, args)
}
private fun parseId(
xmlId: String,
rFilePackage: String,
xmlPosition: XmlPosition
): ResReference {
val ref = parseReference(xmlId, rFilePackage)
if (ref?.isId() == true) {
return ref
}
context.logger.error(NavParserErrors.invalidId(xmlId), xmlPosition)
return context.createStubId()
}
}
internal fun inferArgument(name: String, defaultValue: String, rFilePackage: String): Argument {
val reference = parseReference(defaultValue, rFilePackage)
if (reference != null) {
val type = when (reference.resType) {
"color", "dimen", "integer" -> IntType
"bool" -> BoolType
"string" -> StringType
else -> ReferenceType
}
return Argument(name, type, ReferenceValue(reference))
}
val longValue = parseLongValue(defaultValue)
if (longValue != null) {
return Argument(name, LongType, longValue)
}
val intValue = parseIntValue(defaultValue)
if (intValue != null) {
return Argument(name, IntType, intValue)
}
val floatValue = parseFloatValue(defaultValue)
if (floatValue != null) {
return Argument(name, FloatType, floatValue)
}
val boolValue = parseBoolean(defaultValue)
if (boolValue != null) {
return Argument(name, BoolType, boolValue)
}
return if (defaultValue == VALUE_NULL) {
Argument(name, StringType, NullValue, true)
} else {
Argument(name, StringType, StringValue(defaultValue))
}
}
// @[+][package:]id/resource_name -> package.R.id.resource_name
private val RESOURCE_REGEX = Regex("^@[+]?(.+?:)?(.+?)/(.+)$")
internal fun parseReference(xmlValue: String, rFilePackage: String): ResReference? {
val matchEntire = RESOURCE_REGEX.matchEntire(xmlValue) ?: return null
val groups = matchEntire.groupValues
val resourceName = groups.last()
val resType = groups[groups.size - 2]
val packageName = if (groups[1].isNotEmpty()) groups[1].removeSuffix(":") else rFilePackage
return ResReference(packageName, resType, resourceName)
}
internal fun parseIntValue(value: String): IntValue? {
try {
if (value.startsWith("0x")) {
Integer.parseUnsignedInt(value.substring(2), 16)
} else {
Integer.parseInt(value)
}
} catch (ex: NumberFormatException) {
return null
}
return IntValue(value)
}
internal fun parseLongValue(value: String): LongValue? {
if (!value.endsWith('L')) {
return null
}
try {
val normalizedValue = value.substringBeforeLast('L')
if (normalizedValue.startsWith("0x")) {
normalizedValue.substring(2).toLong(16)
} else {
normalizedValue.toLong()
}
} catch (ex: NumberFormatException) {
return null
}
return LongValue(value)
}
private fun parseFloatValue(value: String): FloatValue? =
value.toFloatOrNull()?.let { FloatValue(value) }
private fun parseBoolean(value: String): BooleanValue? {
if (value == VALUE_TRUE || value == VALUE_FALSE) {
return BooleanValue(value)
}
return null
}