1 /*
<lambda>null2 * Copyright (C) 2024 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 com.android.tools.metalava.apilevels
18
19 import com.android.tools.metalava.SdkExtension
20 import javax.xml.parsers.SAXParserFactory
21 import org.xml.sax.Attributes
22 import org.xml.sax.helpers.DefaultHandler
23
24 /** Encapsulates information read from the `sdk-extension-info.xml` file. */
25 class SdkExtensionInfo(
26 /** Information retrieved from `<sdk>` elements. */
27 val availableSdkExtensions: AvailableSdkExtensions,
28
29 /** Information retrieved from `<symbol>` elements, organized by jar. */
30 private val extensionsByJar: Map<String, ApiToExtensionsMap>,
31 ) {
32 /**
33 * An empty [ApiToExtensionsMap], returned from [extensionsMapForJarOrEmpty] if a jar specific
34 * map could not be found.
35 *
36 * This is given [availableSdkExtensions] as it will be used to check the validity of sdk names
37 * even if there is no information about the symbols.
38 */
39 private val empty = ApiToExtensionsMap(availableSdkExtensions, Node("<empty>"))
40
41 /**
42 * Get the [ApiToExtensionsMap] for [jar], returning an empty map if no specific map for [jar]
43 * could be found.
44 */
45 fun extensionsMapForJarOrEmpty(jar: String) = extensionsByJar[jar] ?: empty
46
47 companion object {
48 /**
49 * Create an ApiToExtensionsMap from a list of text based rules.
50 *
51 * The input is XML:
52 *
53 * <?xml version="1.0" encoding="utf-8"?>
54 * <sdk-extensions-info version="1">
55 * <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
56 * <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
57 * </sdk-extensions-info>
58 *
59 * The <sdk> and <symbol> tags may be repeated.
60 * - <name> is a long name for the SDK, e.g. "R Extensions".
61 * - <short-name> is a short name for the SDK, e.g. "R-ext".
62 * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
63 * Android SDK ID (0).
64 * - <jar> is the jar file symbol belongs to, named after the jar file in
65 * prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
66 * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
67 * to look up the version of the corresponding SDK, e.g.
68 * "android/os/Build$VERSION_CODES$R"
69 * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
70 * string (or prefix thereof terminated before . or $), which matches anything with that
71 * prefix. Note that arguments and return values of methods are omitted (and there is no
72 * way to distinguish overloaded methods).
73 * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
74 * <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
75 *
76 * It is an error to specify the same <jar> and <pattern> pair twice.
77 *
78 * A more specific <symbol> rule has higher precedence than a less specific rule.
79 *
80 * @param filterByJar jar file to limit lookups to: ignore symbols not present in this jar
81 * file
82 * @param xml XML as described above
83 * @throws IllegalArgumentException if the XML is malformed
84 */
85 fun fromXml(xml: String): SdkExtensionInfo {
86 val sdkExtensions = mutableSetOf<SdkExtension>()
87 val allSeenExtensions = mutableSetOf<String>()
88
89 // Map from jar name to the root node.
90 val jarToRoot = mutableMapOf<String, Node>()
91
92 val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
93 try {
94 parser.parse(
95 xml.byteInputStream(),
96 object : DefaultHandler() {
97 override fun startElement(
98 uri: String,
99 localName: String,
100 qualifiedName: String,
101 attributes: Attributes
102 ) {
103 when (qualifiedName) {
104 "sdk" -> {
105 val id = attributes.getIntOrThrow(qualifiedName, "id")
106 val shortname =
107 attributes.getStringOrThrow(qualifiedName, "shortname")
108 val name = attributes.getStringOrThrow(qualifiedName, "name")
109 val reference =
110 attributes.getStringOrThrow(qualifiedName, "reference")
111 sdkExtensions.add(
112 SdkExtension.fromXmlAttributes(
113 id,
114 shortname,
115 name,
116 reference,
117 )
118 )
119 }
120 "symbol" -> {
121 val jar = attributes.getStringOrThrow(qualifiedName, "jar")
122 // Get the root node for the jar, creating one if needed.
123 val rootForJar =
124 jarToRoot.computeIfAbsent(jar) { Node("<jar $jar>") }
125
126 val sdks =
127 attributes
128 .getStringOrThrow(qualifiedName, "sdks")
129 .split(',')
130 if (sdks != sdks.distinct()) {
131 throw IllegalArgumentException(
132 "symbol lists the same SDK multiple times: '$sdks'"
133 )
134 }
135 allSeenExtensions.addAll(sdks)
136 val pattern =
137 attributes.getStringOrThrow(qualifiedName, "pattern")
138 if (pattern == "*") {
139 rootForJar.extensions = sdks
140 return
141 }
142 // pattern is com.example.Foo, add nodes:
143 // "com" -> "example" -> "Foo"
144 val parts = pattern.splitIntoBreadcrumbs()
145 var node = rootForJar
146 for (name in parts) {
147 node = node.children.addNode(name)
148 }
149 if (node.extensions.isNotEmpty()) {
150 throw IllegalArgumentException(
151 "duplicate pattern: $pattern"
152 )
153 }
154 node.extensions = sdks
155 }
156 }
157 }
158 }
159 )
160 } catch (e: Throwable) {
161 throw IllegalArgumentException("failed to parse xml", e)
162 }
163
164 val availableSdkExtensions = AvailableSdkExtensions(sdkExtensions)
165
166 // verify: all rules refer to declared SDKs
167 for (ext in allSeenExtensions) {
168 if (!availableSdkExtensions.containsSdkExtension(ext)) {
169 throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
170 }
171 }
172
173 // Transform the map from jar to root node into a map from jar to ApiToExtensionsMap.
174 val extensionsByJar =
175 jarToRoot.entries.associate { (jar, root) ->
176 jar to ApiToExtensionsMap(availableSdkExtensions, root)
177 }
178 return SdkExtensionInfo(availableSdkExtensions, extensionsByJar)
179 }
180 }
181 }
182
MutableSetnull183 private fun MutableSet<Node>.addNode(name: String): Node {
184 findNode(name)?.let {
185 return it
186 }
187 val node = Node(name)
188 add(node)
189 return node
190 }
191
Attributesnull192 private fun Attributes.getStringOrThrow(tag: String, attr: String): String =
193 getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
194
195 private fun Attributes.getIntOrThrow(tag: String, attr: String): Int =
196 getStringOrThrow(tag, attr).toIntOrNull()
197 ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
198