• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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