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(""") // $NON-NLS-1$ 228 } 229 '<' -> { 230 append("<") // $NON-NLS-1$ 231 } 232 '\'' -> { 233 append("'") // $NON-NLS-1$ 234 } 235 '&' -> { 236 append("&") // $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