1 /*
2 * Copyright (C) 2022 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 package com.android.tools.metalava.apilevels
17
18 import com.android.tools.metalava.SdkIdentifier
19 import org.xml.sax.Attributes
20 import org.xml.sax.helpers.DefaultHandler
21 import javax.xml.parsers.SAXParserFactory
22
23 /**
24 * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
25 * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
26 *
27 * Internally, the filers are represented as a tree, where each node in the tree matches a part of a
28 * package, class or member name. For example, given the patterns
29 *
30 * com.example.Foo -> [A]
31 * com.example.Foo#someMethod -> [B]
32 * com.example.Bar -> [A, C]
33 *
34 * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
35 * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part
36 * of both A and C), the internal tree looks like
37 *
38 * root -> null
39 * com -> null
40 * example -> null
41 * Foo -> [A]
42 * someMethod -> [B]
43 * Bar -> [A, C]
44 */
45 class ApiToExtensionsMap private constructor(
46 private val sdkIdentifiers: Set<SdkIdentifier>,
47 private val root: Node,
48 ) {
isEmptynull49 fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
50
51 fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
52
53 fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
54 getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
55
56 fun getExtensions(what: String): List<String> {
57 // Special case: getExtensionVersion is not part of an extension
58 val sdkExtensions = "android.os.ext.SdkExtensions"
59 if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
60 return listOf()
61 }
62
63 val parts = what.split(REGEX_DELIMITERS)
64
65 var lastSeenExtensions = root.extensions
66 var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
67 if (node.extensions.isNotEmpty()) {
68 lastSeenExtensions = node.extensions
69 }
70
71 for (part in parts.stream().skip(1)) {
72 node = node.children.findNode(part) ?: break
73 if (node.extensions.isNotEmpty()) {
74 lastSeenExtensions = node.extensions
75 }
76 }
77 return lastSeenExtensions
78 }
79
getSdkIdentifiersnull80 fun getSdkIdentifiers(): Set<SdkIdentifier> = sdkIdentifiers.toSet()
81
82 /**
83 * Construct a `sdks` attribute value
84 *
85 * `sdks` is an XML attribute on class, method and fields in the XML generated by ARG_GENERATE_API_LEVELS.
86 * It expresses in what SDKs an API exist, and in which version of each SDK it was first introduced;
87 * `sdks` replaces the `since` attribute.
88 *
89 * The format of `sdks` is
90 *
91 * sdks="ext:version[,ext:version[,...]]
92 *
93 * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was introduced.
94 *
95 * The returned string is guaranteed to be one of
96 *
97 * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
98 * - list of (extensions,finalized_version) pairs
99 * - ANDROID_SDK:finalized_dessert
100 * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
101 *
102 * See go/mainline-sdk-api-versions-xml for more information.
103 *
104 * @param androidSince Android dessert version in which this symbol was finalized, or notFinalizedValue
105 * if this symbol has not been finalized in an Android dessert
106 * @param notFinalizedValue value used together with the Android SDK ID to indicate that this symbol
107 * has not been finalized at all
108 * @param extensions names of the SDK extensions in which this symbol has been finalized (may be non-empty
109 * even if extensionsSince is ApiElement.NEVER)
110 * @param extensionsSince the version of the SDK extensions in which this API was initially introduced
111 * (same value for all SDK extensions), or ApiElement.NEVER if this symbol
112 * has not been finalized in any SDK extension (regardless of the extensions argument)
113 * @return an `sdks` value suitable for including verbatim in XML
114 */
115 fun calculateSdksAttr(
116 androidSince: Int,
117 notFinalizedValue: Int,
118 extensions: List<String>,
119 extensionsSince: Int
120 ): String {
121 // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
122 if (androidSince == notFinalizedValue && extensionsSince == ApiElement.NEVER) {
123 return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
124 }
125
126 val versions = mutableSetOf<String>()
127 // Only include SDK extensions if the symbol has been finalized in at least one
128 if (extensionsSince != ApiElement.NEVER) {
129 for (ext in extensions) {
130 val ident = sdkIdentifiers.find {
131 it.shortname == ext
132 } ?: throw IllegalStateException("unknown extension SDK \"$ext\"")
133 assert(ident.id != ANDROID_PLATFORM_SDK_ID) // invariant
134 versions.add("${ident.id}:$extensionsSince")
135 }
136 }
137
138 // Only include the Android SDK in `sdks` if
139 // - the symbol has been finalized in an Android dessert, and
140 // - the symbol has been finalized in at least one SDK extension
141 if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
142 versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
143 }
144 return versions.joinToString(",")
145 }
146
147 companion object {
148 // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
149 // to express when an API first appeared in an SDK.
150 const val ANDROID_PLATFORM_SDK_ID = 0
151
152 private val REGEX_DELIMITERS = Regex("[.#$]")
153
154 /*
155 * Create an ApiToExtensionsMap from a list of text based rules.
156 *
157 * The input is XML:
158 *
159 * <?xml version="1.0" encoding="utf-8"?>
160 * <sdk-extensions-info version="1">
161 * <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
162 * <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
163 * </sdk-extensions-info>
164 *
165 * The <sdk> and <symbol> tags may be repeated.
166 *
167 * - <name> is a long name for the SDK, e.g. "R Extensions".
168 *
169 * - <short-name> is a short name for the SDK, e.g. "R-ext".
170 *
171 * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
172 * Android SDK ID (0).
173 *
174 * - <jar> is the jar file symbol belongs to, named after the jar file in
175 * prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
176 *
177 * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
178 * to look up the version of the corresponding SDK, e.g.
179 * "android/os/Build$VERSION_CODES$R"
180 *
181 * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
182 * string (or prefix thereof terminated before . or $), which matches anything with that
183 * prefix. Note that arguments and return values of methods are omitted (and there is no
184 * way to distinguish overloaded methods).
185 *
186 * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
187 * <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
188 *
189 * It is an error to specify the same <jar> and <pattern> pair twice.
190 *
191 * A more specific <symbol> rule has higher precedence than a less specific rule.
192 *
193 * @param jar jar file to limit lookups to: ignore symbols not present in this jar file
194 * @param xml XML as described above
195 * @throws IllegalArgumentException if the XML is malformed
196 */
fromXmlnull197 fun fromXml(filterByJar: String, xml: String): ApiToExtensionsMap {
198 val root = Node("<root>")
199 val sdkIdentifiers = mutableSetOf<SdkIdentifier>()
200 val allSeenExtensions = mutableSetOf<String>()
201
202 val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
203 try {
204 parser.parse(
205 xml.byteInputStream(),
206 object : DefaultHandler() {
207 override fun startElement(uri: String, localName: String, qualifiedName: String, attributes: Attributes) {
208 when (qualifiedName) {
209 "sdk" -> {
210 val id = attributes.getIntOrThrow(qualifiedName, "id")
211 val shortname = attributes.getStringOrThrow(qualifiedName, "shortname")
212 val name = attributes.getStringOrThrow(qualifiedName, "name")
213 val reference = attributes.getStringOrThrow(qualifiedName, "reference")
214 sdkIdentifiers.add(SdkIdentifier(id, shortname, name, reference))
215 }
216 "symbol" -> {
217 val jar = attributes.getStringOrThrow(qualifiedName, "jar")
218 if (jar != filterByJar) {
219 return
220 }
221 val sdks = attributes.getStringOrThrow(qualifiedName, "sdks").split(',')
222 if (sdks != sdks.distinct()) {
223 throw IllegalArgumentException("symbol lists the same SDK multiple times: '$sdks'")
224 }
225 allSeenExtensions.addAll(sdks)
226 val pattern = attributes.getStringOrThrow(qualifiedName, "pattern")
227 if (pattern == "*") {
228 root.extensions = sdks
229 return
230 }
231 // add each part of the pattern as separate nodes, e.g. if pattern is
232 // com.example.Foo, add nodes, "com" -> "example" -> "Foo"
233 val parts = pattern.split(REGEX_DELIMITERS)
234 var node = root.children.addNode(parts[0])
235 for (name in parts.stream().skip(1)) {
236 node = node.children.addNode(name)
237 }
238 if (node.extensions.isNotEmpty()) {
239 throw IllegalArgumentException("duplicate pattern: $pattern")
240 }
241 node.extensions = sdks
242 }
243 }
244 }
245 }
246 )
247 } catch (e: Throwable) {
248 throw IllegalArgumentException("failed to parse xml", e)
249 }
250
251 // verify: the predefined Android platform SDK ID is not reused as an extension SDK ID
252 if (sdkIdentifiers.any { it.id == ANDROID_PLATFORM_SDK_ID }) {
253 throw IllegalArgumentException("bad SDK definition: the ID $ANDROID_PLATFORM_SDK_ID is reserved for the Android platform SDK")
254 }
255
256 // verify: all rules refer to declared SDKs
257 val allSdkNames = sdkIdentifiers.map { it.shortname }.toList()
258 for (ext in allSeenExtensions) {
259 if (!allSdkNames.contains(ext)) {
260 throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
261 }
262 }
263
264 // verify: no duplicate SDK IDs
265 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.id }.size) {
266 throw IllegalArgumentException("bad SDK definitions: duplicate SDK IDs")
267 }
268
269 // verify: no duplicate SDK names
270 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.shortname }.size) {
271 throw IllegalArgumentException("bad SDK definitions: duplicate SDK short names")
272 }
273
274 // verify: no duplicate SDK names
275 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.name }.size) {
276 throw IllegalArgumentException("bad SDK definitions: duplicate SDK names")
277 }
278
279 // verify: no duplicate SDK references
280 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.reference }.size) {
281 throw IllegalArgumentException("bad SDK definitions: duplicate SDK references")
282 }
283
284 return ApiToExtensionsMap(sdkIdentifiers, root)
285 }
286 }
287 }
288
MutableSetnull289 private fun MutableSet<Node>.addNode(name: String): Node {
290 findNode(name)?.let {
291 return it
292 }
293 val node = Node(name)
294 add(node)
295 return node
296 }
297
Attributesnull298 private fun Attributes.getStringOrThrow(tag: String, attr: String): String = getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
299
300 private fun Attributes.getIntOrThrow(tag: String, attr: String): Int = getStringOrThrow(tag, attr).toIntOrNull() ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
301
302 private fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
303
Stringnull304 private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
305
306 private class Node(val breadcrumb: String) {
307 var extensions: List<String> = emptyList()
308 val children: MutableSet<Node> = mutableSetOf()
309 }
310