1+ /*
2+ * This file is part of AndroidIDE.
3+ *
4+ * AndroidIDE is free software: you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation, either version 3 of the License, or
7+ * (at your option) any later version.
8+ *
9+ * AndroidIDE is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU General Public License
15+ * along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
16+ */
17+
18+ package com.itsaky.androidide.lsp.java.indexing.apiinfo
19+
20+ import androidx.collection.mutableIntObjectMapOf
21+ import jaxp.xml.namespace.QName
22+ import jaxp.xml.stream.XMLInputFactory
23+ import jaxp.xml.stream.events.Attribute
24+ import jaxp.xml.stream.events.EndElement
25+ import jaxp.xml.stream.events.StartElement
26+ import org.slf4j.LoggerFactory
27+ import java.io.InputStream
28+
29+ /* *
30+ * Parser for parsing `api-versions.xml` file from the Android SDK and building [ApiInfo] models.
31+ *
32+ * @author Akash Yadav
33+ */
34+ class ApiInfoParser {
35+
36+ private val apiInfos = mutableIntObjectMapOf<ApiInfo >()
37+ private val apiInfo = HashMap <String , Pair <ApiInfo , HashMap <String , ApiInfo >>>()
38+
39+ private var apiVersion: Int? = null
40+ private var currentClass: String? = null
41+
42+ companion object {
43+ private val log = LoggerFactory .getLogger(ApiInfoParser ::class .java)
44+ }
45+
46+ fun parse (apiVersionsXml : InputStream ) {
47+ if (apiVersionsXml.available() <= 0 ) {
48+ log.warn(" api-versions.xml InputStream is empty" )
49+ return
50+ }
51+
52+ val inputFactory = XMLInputFactory .newInstance()
53+ val reader = inputFactory.createXMLEventReader(apiVersionsXml)
54+
55+ while (reader.hasNext()) {
56+ val event = reader.nextEvent()
57+ if (event.isStartElement) {
58+ consumeStartElement(event.asStartElement())
59+ continue
60+ }
61+
62+ if (event.isEndElement) {
63+ consumeEndElement(event.asEndElement())
64+ }
65+ }
66+ }
67+
68+ /* *
69+ * Returns [ApiInfo] for the given class.
70+ */
71+ fun getClassInfo (className : String ): ApiInfo ? {
72+ return apiInfo[className]?.first
73+ }
74+
75+ /* *
76+ * Returns [ApiInfo] for the given class and member.
77+ */
78+ fun getMemberInfo (className : String , memberName : String ): ApiInfo ? {
79+ return apiInfo[className]?.second?.get(memberName)
80+ }
81+
82+ /* *
83+ * Removes and returns the [ApiInfo] for the given class and member. This also removes the [ApiInfo]
84+ * for the class if the all the members of the class have been removed.
85+ */
86+ fun removeMemberInfo (className : String , memberName : String ): ApiInfo ? {
87+ return apiInfo[className]?.second?.remove(memberName).also {
88+ if (apiInfo[className]?.second?.isEmpty() == true ) {
89+ apiInfo.remove(className)
90+ }
91+ }
92+ }
93+
94+ private fun consumeStartElement (event : StartElement ) {
95+ when (event.name.localPart) {
96+ " api" -> apiVersion = event.getAttributeByName(QName (" version" )).value.toInt()
97+ " class" -> consumeClass(event)
98+ " field" -> consumeMember(event, " field" )
99+ " method" -> consumeMember(event, " method" )
100+ }
101+ }
102+
103+ private fun consumeClass (event : StartElement ) {
104+ checkNotNull(apiVersion) {
105+ " <class> element must be inside <api> element"
106+ }
107+
108+ check(currentClass == null ) {
109+ " <class> elements cannot be nested"
110+ }
111+
112+ val (name, versions) = event.parseAttrs()
113+ check(! apiInfo.containsKey(name)) {
114+ " Duplicate class entry: $name "
115+ }
116+
117+ val apiInfo = apiInfos.getOrPut(versions) {
118+ createApiInfo(versions)
119+ }
120+
121+ this .apiInfo[name] = apiInfo to HashMap ()
122+ this .currentClass = name
123+ }
124+
125+ private fun consumeMember (event : StartElement , memberType : String ) {
126+ val (name, versions) = event.parseAttrs()
127+ val currentClass = checkNotNull(currentClass) {
128+ " <${memberType} > element must be inside <class> element"
129+ }
130+ check(! apiInfo.containsKey(name)) {
131+ " Duplicate $memberType entry in class $currentClass : $name "
132+ }
133+
134+ val (_, members) = apiInfo[currentClass]!!
135+ val existing = members.put(name, createApiInfo(versions))
136+ check(existing == null ) {
137+ " Duplicate $memberType entry in class $currentClass : $name "
138+ }
139+ }
140+
141+ private fun consumeEndElement (element : EndElement ) {
142+ when (element.name.localPart) {
143+ " api" -> apiVersion = null
144+ " class" -> currentClass = null
145+ }
146+ }
147+
148+ private fun StartElement.parseAttrs (): Pair <String , Int > {
149+ var name: String? = null
150+ var since = 1
151+ var deprecated = 0
152+ var removed = 0
153+
154+ attributes.forEach { attribute ->
155+ attribute as Attribute
156+
157+ when (attribute.name.localPart) {
158+ " name" -> name = attribute.value
159+ " since" -> since = attribute.value.toInt()
160+ " deprecated" -> deprecated = attribute.value.toInt()
161+ " removed" -> removed = attribute.value.toInt()
162+ }
163+ }
164+
165+ checkNotNull(name) {
166+ " Missing name attribute"
167+ }
168+
169+ // Android API versions would not exceed 255, right?
170+ check(since in 1 .. 255 && deprecated in 0 .. 255 && removed in 0 .. 255 ) {
171+ " Invalid version: $since , $deprecated , $removed "
172+ }
173+
174+ val version = (since shl 16 ) + (deprecated shl 8 ) + removed
175+ return name!! to version
176+ }
177+
178+ private fun createApiInfo (versions : Int ): ApiInfo {
179+ val since = (versions shr 16 ) and 0x000000FF
180+ val deprecated = (versions shr 8 ) and 0x000000FF
181+ val removed = versions and 0x000000FF
182+ return ApiInfo .newInstance(since = since, deprecatedIn = deprecated, removedIn = removed)
183+ }
184+ }
0 commit comments