1 /*
<lambda>null2  * Copyright 2025 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 androidx.appfunctions.compiler.processors
18 
19 import androidx.appfunctions.compiler.core.AnnotatedAppFunctionSerializableProxy.ResolvedAnnotatedSerializableProxies
20 import androidx.appfunctions.compiler.core.AnnotatedAppFunctions
21 import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
22 import androidx.appfunctions.metadata.AppFunctionComponentsMetadata
23 import androidx.appfunctions.metadata.AppFunctionDataTypeMetadata
24 import androidx.appfunctions.metadata.CompileTimeAppFunctionMetadata
25 import com.google.devtools.ksp.processing.CodeGenerator
26 import com.google.devtools.ksp.processing.Dependencies
27 import com.google.devtools.ksp.processing.Resolver
28 import com.google.devtools.ksp.processing.SymbolProcessor
29 import com.google.devtools.ksp.symbol.KSAnnotated
30 import javax.xml.parsers.DocumentBuilderFactory
31 import javax.xml.transform.OutputKeys
32 import javax.xml.transform.TransformerFactory
33 import javax.xml.transform.dom.DOMSource
34 import javax.xml.transform.stream.StreamResult
35 import kotlin.reflect.KProperty1
36 import org.w3c.dom.Document
37 import org.w3c.dom.Element
38 
39 /**
40  * Generates AppFunction's index xml file with all properties of [CompileTimeAppFunctionMetadata]
41  * for the AppSearch indexer to index.
42  *
43  * The generator would write an XML file as `/assets/app_functions_dynamic_schema.xml`. The file
44  * would be packaged into the APK's asset when assembled, so that the AppSearch indexer can look up
45  * the asset and inject metadata into platform AppSearch database accordingly.
46  */
47 class AppFunctionIndexXmlProcessor(
48     private val codeGenerator: CodeGenerator,
49 ) : SymbolProcessor {
50 
51     override fun process(resolver: Resolver): List<KSAnnotated> {
52         val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
53         val resolvedAnnotatedSerializableProxies =
54             ResolvedAnnotatedSerializableProxies(
55                 appFunctionSymbolResolver.resolveAllAnnotatedSerializableProxiesFromModule()
56             )
57         generateIndexXml(
58             appFunctionSymbolResolver.getAnnotatedAppFunctionsFromAllModules(),
59             resolvedAnnotatedSerializableProxies
60         )
61         return emptyList()
62     }
63 
64     /**
65      * Generates AppFunction's index xml files for indexer in App Search.
66      *
67      * @param appFunctionsByClass a collection of functions annotated with @AppFunction
68      * @param resolvedAnnotatedSerializableProxies a collection of resolved annotated serializable
69      *   proxies
70      */
71     private fun generateIndexXml(
72         appFunctionsByClass: List<AnnotatedAppFunctions>,
73         resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies
74     ) {
75         if (appFunctionsByClass.isEmpty()) {
76             return
77         }
78         writeXmlFile(appFunctionsByClass, resolvedAnnotatedSerializableProxies)
79     }
80 
81     private fun writeXmlFile(
82         appFunctionsByClass: List<AnnotatedAppFunctions>,
83         resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies
84     ) {
85         val appFunctionMetadataList =
86             appFunctionsByClass.flatMap {
87                 it.createAppFunctionMetadataList(resolvedAnnotatedSerializableProxies)
88             }
89 
90         val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
91         val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true }
92 
93         val appFunctionsElement = xmlDocument.createElement(APP_FUNCTIONS_ELEMENTS_TAG)
94         xmlDocument.appendChild(appFunctionsElement)
95 
96         val aggregatedDataTypes: MutableMap<String, AppFunctionDataTypeMetadata> = mutableMapOf()
97         for (appFunctionMetadata in appFunctionMetadataList) {
98             appFunctionMetadata.components.dataTypes.forEach { (objectKey, dataTypeMetadata) ->
99                 aggregatedDataTypes.putIfAbsent(objectKey, dataTypeMetadata)
100             }
101             val sanitizedAppFunctionMetadata =
102                 appFunctionMetadata.copy(components = AppFunctionComponentsMetadata())
103 
104             val appFunctionElement =
105                 xmlDocument.createElementWithInstance(
106                     APP_FUNCTION_ITEM_TAG,
107                     sanitizedAppFunctionMetadata.toAppFunctionMetadataDocument(),
108                     // Below properties are named differently in platform's
109                     // AppFunctionStaticMetadata GD hence we encode them in XML accordingly.
110                     customTagNames = mapOf("isEnabledByDefault" to "enabledByDefault"),
111                     // Irrelevant properties that do not need to be encoded in XML.
112                     skipProperties = setOf("namespace")
113                 )
114             appFunctionElement.appendChild(
115                 xmlDocument.createElementWithTextNode(
116                     APP_FUNCTION_ID_TAG,
117                     sanitizedAppFunctionMetadata.id
118                 )
119             )
120             appFunctionsElement.appendChild(appFunctionElement)
121         }
122 
123         val componentElement =
124             xmlDocument.createElementWithInstance(
125                 COMPONENT_ITEM_TAG,
126                 AppFunctionComponentsMetadata(aggregatedDataTypes)
127                     .toAppFunctionComponentsMetadataDocument(),
128                 // Below properties are named differently in platform's
129                 // AppFunctionStaticMetadata GD hence we encode them in XML accordingly.
130                 customTagNames = mapOf("isEnabledByDefault" to "enabledByDefault"),
131                 // Irrelevant properties that do not need to be encoded in XML.
132                 skipProperties = setOf("namespace")
133             )
134         appFunctionsElement.appendChild(componentElement)
135 
136         val transformer =
137             TransformerFactory.newInstance().newTransformer().apply {
138                 setOutputProperty(OutputKeys.INDENT, "yes")
139                 setOutputProperty(OutputKeys.ENCODING, "UTF-8")
140                 setOutputProperty(OutputKeys.VERSION, "1.0")
141                 setOutputProperty(OutputKeys.STANDALONE, "yes")
142             }
143 
144         codeGenerator
145             .createNewFile(
146                 Dependencies(
147                     aggregating = true,
148                     *appFunctionsByClass.flatMap { it.getSourceFiles() }.toTypedArray()
149                 ),
150                 XML_PACKAGE_NAME,
151                 XML_FILE_NAME,
152                 XML_EXTENSION
153             )
154             .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) }
155     }
156 
157     /**
158      * Creates an XML element from [instance], including nested structures and collections.
159      *
160      * This function recursively converts a data class instance into an XML element, handling nested
161      * data classes and collections appropriately. For non-data-class values, it creates text nodes.
162      *
163      * @param elementName The name of the root XML element to create.
164      * @param instance The instance to convert into an XML structure.
165      * @param customTagNames Mapping of property names to customized tag names when creating nodes.
166      * @param skipProperties Property names to skip when creating XML elements.
167      * @return The created XML element representing the instance.
168      */
169     private fun Document.createElementWithInstance(
170         elementName: String,
171         instance: Any,
172         customTagNames: Map<String, String>,
173         skipProperties: Set<String>,
174     ): Element {
175         if (instance.isPrimitiveType()) {
176             return createElementWithTextNode(elementName, instance.toString())
177         }
178 
179         val doc = this
180         val element = createElement(elementName)
181 
182         for (property in instance::class.members.filterIsInstance<KProperty1<Any, *>>()) {
183 
184             if (property.name in skipProperties) continue
185 
186             val value = property.get(instance) ?: continue
187             val propertyName = customTagNames[property.name] ?: property.name
188 
189             when {
190                 value is List<*> ->
191                     value
192                         .filterNotNull()
193                         .map { item ->
194                             doc.createElementWithInstance(
195                                 propertyName,
196                                 item,
197                                 customTagNames,
198                                 skipProperties
199                             )
200                         }
201                         .forEach(element::appendChild)
202                 else ->
203                     element.appendChild(
204                         doc.createElementWithInstance(
205                             propertyName,
206                             value,
207                             customTagNames,
208                             skipProperties
209                         )
210                     )
211             }
212         }
213 
214         return element
215     }
216 
217     private fun Any.isPrimitiveType(): Boolean {
218         return this is Byte ||
219             this is Short ||
220             this is Int ||
221             this is Long ||
222             this is Float ||
223             this is Double ||
224             this is Char ||
225             this is Boolean ||
226             this is String
227     }
228 
229     private fun Document.createElementWithTextNode(elementName: String, text: String): Element =
230         createElement(elementName).apply { appendChild(createTextNode(text)) }
231 
232     private companion object {
233         const val XML_PACKAGE_NAME = "assets"
234         const val XML_FILE_NAME = "app_functions_v2"
235         const val XML_EXTENSION = "xml"
236         const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions"
237         const val APP_FUNCTION_ITEM_TAG = "appfunction"
238         const val COMPONENT_ITEM_TAG = "components"
239         const val APP_FUNCTION_ID_TAG = "functionId"
240     }
241 }
242