• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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 java.io.PrintWriter
20 import java.util.TreeSet
21 
22 /**
23  * Printer that will write an XML representation of an [Api] instance.
24  *
25  * @param availableSdkExtensions the optional set of [AvailableSdkExtensions].
26  * @param versionedApis the list of all the [VersionedApi]s that will provide information for the
27  *   [Api]. Must include at least one [VersionedApi]. The API version of the first is used to
28  *   populate the `<api min="..."...>` attribute, if it is later than version `1`.
29  */
30 class ApiXmlPrinter(
31     private val availableSdkExtensions: AvailableSdkExtensions?,
32     versionedApis: List<VersionedApi>,
33 ) : ApiPrinter {
34     /** The set of versions, sorted from lowest to highest. */
<lambda>null35     private val sortedVersions = versionedApis.mapTo(TreeSet()) { it.apiVersion }
36 
37     /** Get the first [ApiVersion]. */
38     private val firstApiVersion = sortedVersions.first()
39 
40     /**
41      * Map from version to the next version. This is used to compute the version in which an API
42      * element was removed by finding the version after the version it was last present in.
43      */
44     private val versionToNext = sortedVersions.zipWithNext().toMap()
45 
46     /** True if the [Api] being printed has any minor versions. */
<lambda>null47     private val hasMinorVersions = sortedVersions.any { it.minor != null }
48 
printnull49     override fun print(api: Api, writer: PrintWriter) {
50         writer.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
51         api.print(writer, availableSdkExtensions)
52     }
53 
toStringnull54     override fun toString() = "XML"
55 
56     /**
57      * Prints the whole API definition to a writer.
58      *
59      * @param writer the writer to which the XML elements will be written.
60      */
61     private fun Api.print(writer: PrintWriter, availableSdkExtensions: AvailableSdkExtensions?) {
62         // Select the lowest version that supports the necessary capabilities.
63         val fileVersion = if (hasMinorVersions) 4 else 3
64 
65         writer.print("<api version=\"$fileVersion\"")
66         if (firstApiVersion > DEFAULT_MIN_VERSION) {
67             writer.print(" min=\"$firstApiVersion\"")
68         }
69         writer.println(">")
70         if (availableSdkExtensions != null) {
71             for (sdkExtension in availableSdkExtensions.sdkExtensions) {
72                 writer.println(
73                     String.format(
74                         "\t<sdk id=\"%d\" shortname=\"%s\" name=\"%s\" reference=\"%s\"/>",
75                         sdkExtension.id,
76                         sdkExtension.shortname,
77                         sdkExtension.name,
78                         sdkExtension.reference,
79                     )
80                 )
81             }
82         }
83         print(classes, "class", "\t", writer)
84         printClosingTag("api", "", writer)
85     }
86 
87     /**
88      * Prints homogeneous XML elements to a writer. Each element is printed on a separate line.
89      * Attributes with values matching the parent API element are omitted.
90      *
91      * @param elements the elements to print
92      * @param tag the tag of the XML elements
93      * @param indent the whitespace prefix to insert before each XML element
94      * @param writer the writer to which the XML elements will be written.
95      */
printnull96     private fun ParentApiElement.print(
97         elements: Collection<ApiElement>,
98         tag: String?,
99         indent: String,
100         writer: PrintWriter,
101     ) {
102         for (element in elements.sorted()) {
103             element.print(tag, this, indent, writer)
104         }
105     }
106 
107     /**
108      * Prints an XML representation of the element to a writer terminated by a line break.
109      * Attributes with values matching the parent API element are omitted.
110      *
111      * @param tag the tag of the XML element
112      * @param parentApiElement the parent API element
113      * @param indent the whitespace prefix to insert before the XML element
114      * @param writer the writer to which the XML element will be written.
115      */
printnull116     private fun ApiElement.print(
117         tag: String?,
118         parentApiElement: ParentApiElement,
119         indent: String,
120         writer: PrintWriter
121     ) {
122         if (this is ApiClass) printClass(tag, parentApiElement, indent, writer)
123         else print(tag, true, parentApiElement, indent, writer)
124     }
125 
printClassnull126     private fun ApiClass.printClass(
127         tag: String?,
128         parentApiElement: ParentApiElement,
129         indent: String,
130         writer: PrintWriter
131     ) {
132         if (alwaysHidden) {
133             return
134         }
135         print(tag, false, parentApiElement, indent, writer)
136         val innerIndent = indent + '\t'
137         print(superClasses, "extends", innerIndent, writer)
138         print(interfaces, "implements", innerIndent, writer)
139         print(methods, "method", innerIndent, writer)
140         print(fields, "field", innerIndent, writer)
141         printClosingTag(tag, indent, writer)
142     }
143 
144     /**
145      * Prints an XML representation of the element to a writer terminated by a line break.
146      * Attributes with values matching the parent API element are omitted.
147      *
148      * @param tag the tag of the XML element
149      * @param closeTag if true the XML element is terminated by "/>", otherwise the closing tag of
150      *   the element is not printed
151      * @param parentApiElement the parent API element
152      * @param indent the whitespace prefix to insert before the XML element
153      * @param writer the writer to which the XML element will be written.
154      * @see printClosingTag
155      */
printnull156     private fun ApiElement.print(
157         tag: String?,
158         closeTag: Boolean,
159         parentApiElement: ParentApiElement,
160         indent: String?,
161         writer: PrintWriter
162     ) {
163         writer.print(indent)
164         writer.print('<')
165         writer.print(tag)
166         writer.print(" name=\"")
167         writer.print(encodeAttribute(name))
168         if (!isEmpty(mainlineModule) && !isEmpty(sdks)) {
169             writer.print("\" module=\"")
170             writer.print(encodeAttribute(mainlineModule!!))
171         }
172         if (since > parentApiElement.since) {
173             writer.print("\" since=\"")
174             writer.print(since)
175         }
176         if (!isEmpty(sdks) && sdks != parentApiElement.sdks) {
177             writer.print("\" sdks=\"")
178             writer.print(sdks)
179         }
180         if (deprecatedIn != null && deprecatedIn != parentApiElement.deprecatedIn) {
181             writer.print("\" deprecated=\"")
182             writer.print(deprecatedIn)
183         }
184         if (lastPresentIn < parentApiElement.lastPresentIn) {
185             val removedFrom =
186                 versionToNext[lastPresentIn]
187                     ?: error("could not find next version for $lastPresentIn in $name")
188             writer.print("\" removed=\"")
189             writer.print(removedFrom)
190         }
191         writer.print('"')
192         if (closeTag) {
193             writer.print('/')
194         }
195         writer.println('>')
196     }
197 
198     companion object {
199         /** The default minimum [ApiVersion] expected by consumers of `api-versions.xml`. */
200         private val DEFAULT_MIN_VERSION = ApiVersion.fromLevel(1)
201 
202         /**
203          * Prints a closing tag of an XML element terminated by a line break.
204          *
205          * @param tag the tag of the element
206          * @param indent the whitespace prefix to insert before the closing tag
207          * @param writer the writer to which the XML element will be written.
208          */
printClosingTagnull209         private fun printClosingTag(tag: String?, indent: String?, writer: PrintWriter) {
210             writer.print(indent)
211             writer.print("</")
212             writer.print(tag)
213             writer.println('>')
214         }
215 
encodeAttributenull216         private fun encodeAttribute(attribute: String): String {
217             return buildString {
218                 val n = attribute.length
219                 // &, ", ' and < are illegal in attributes; see
220                 // http://www.w3.org/TR/REC-xml/#NT-AttValue
221                 // (' legal in a " string and " is legal in a ' string but here we'll stay on the
222                 // safe
223                 // side).
224                 for (i in 0 until n) {
225                     when (val c = attribute[i]) {
226                         '"' -> {
227                             append("&quot;") // $NON-NLS-1$
228                         }
229                         '<' -> {
230                             append("&lt;") // $NON-NLS-1$
231                         }
232                         '\'' -> {
233                             append("&apos;") // $NON-NLS-1$
234                         }
235                         '&' -> {
236                             append("&amp;") // $NON-NLS-1$
237                         }
238                         else -> {
239                             append(c)
240                         }
241                     }
242                 }
243             }
244         }
245 
isEmptynull246         private fun isEmpty(s: String?): Boolean {
247             return s.isNullOrEmpty()
248         }
249     }
250 }
251