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.AppFunctionMetadataDocument
23 import com.google.devtools.ksp.processing.CodeGenerator
24 import com.google.devtools.ksp.processing.Dependencies
25 import com.google.devtools.ksp.processing.Resolver
26 import com.google.devtools.ksp.processing.SymbolProcessor
27 import com.google.devtools.ksp.symbol.KSAnnotated
28 import javax.xml.parsers.DocumentBuilderFactory
29 import javax.xml.transform.OutputKeys
30 import javax.xml.transform.TransformerFactory
31 import javax.xml.transform.dom.DOMSource
32 import javax.xml.transform.stream.StreamResult
33 import org.w3c.dom.Document
34 import org.w3c.dom.Element
35 
36 /**
37  * Generates AppFunction's index xml file for the legacy AppSearch indexer to index.
38  *
39  * The generator would write an XML file as `/assets/app_functions.xml`. The file would be packaged
40  * into the APK's asset when assembled. So that the AppSearch indexer can look up the asset and
41  * inject metadata into platform AppSearch database accordingly.
42  *
43  * The new indexer will index additional properties based on the schema defined in SDK instead of
44  * the pre-defined one in AppSearch.
45  */
46 class AppFunctionLegacyIndexXmlProcessor(
47     private val codeGenerator: CodeGenerator,
48 ) : SymbolProcessor {
49 
50     override fun process(resolver: Resolver): List<KSAnnotated> {
51         val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
52         val resolvedAnnotatedSerializableProxies =
53             ResolvedAnnotatedSerializableProxies(
54                 appFunctionSymbolResolver.resolveAllAnnotatedSerializableProxiesFromModule()
55             )
56         generateLegacyIndexXml(
57             appFunctionSymbolResolver.getAnnotatedAppFunctionsFromAllModules(),
58             resolvedAnnotatedSerializableProxies
59         )
60         return emptyList()
61     }
62 
63     /**
64      * Generates AppFunction's legacy index xml files for v1 indexer in App Search.
65      *
66      * @param appFunctionsByClass a collection of functions annotated with @AppFunction grouped by
67      *   their enclosing classes.
68      */
69     private fun generateLegacyIndexXml(
70         appFunctionsByClass: List<AnnotatedAppFunctions>,
71         resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies
72     ) {
73         if (appFunctionsByClass.isEmpty()) {
74             return
75         }
76         val appFunctionMetadataList =
77             appFunctionsByClass.flatMap {
78                 it.createAppFunctionMetadataList(resolvedAnnotatedSerializableProxies).map {
79                     it.toAppFunctionMetadataDocument()
80                 }
81             }
82         writeXmlFile(appFunctionMetadataList, appFunctionsByClass)
83     }
84 
85     private fun writeXmlFile(
86         appFunctionMetadataList: List<AppFunctionMetadataDocument>,
87         appFunctionsByClass: List<AnnotatedAppFunctions>,
88     ) {
89         val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
90         val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true }
91 
92         val appFunctionsElement = xmlDocument.createElement(XmlElement.APP_FUNCTIONS_ELEMENTS_TAG)
93         xmlDocument.appendChild(appFunctionsElement)
94 
95         for (appFunctionMetadata in appFunctionMetadataList) {
96             appFunctionsElement.appendChild(
97                 xmlDocument.createAppFunctionElement(appFunctionMetadata)
98             )
99         }
100 
101         val transformer =
102             TransformerFactory.newInstance().newTransformer().apply {
103                 setOutputProperty(OutputKeys.INDENT, "yes")
104                 setOutputProperty(OutputKeys.ENCODING, "UTF-8")
105                 setOutputProperty(OutputKeys.VERSION, "1.0")
106                 setOutputProperty(OutputKeys.STANDALONE, "yes")
107             }
108 
109         codeGenerator
110             .createNewFile(
111                 Dependencies(
112                     aggregating = true,
113                     *appFunctionsByClass.flatMap { it.getSourceFiles() }.toTypedArray()
114                 ),
115                 XML_PACKAGE_NAME,
116                 XML_FILE_NAME,
117                 XML_EXTENSION
118             )
119             .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) }
120     }
121 
122     private fun Document.createAppFunctionElement(
123         appFunctionMetadata: AppFunctionMetadataDocument
124     ): Element =
125         createElement(XmlElement.APP_FUNCTION_ITEM_TAG).apply {
126             appendChild(
127                 createElementWithTextNode(XmlElement.APP_FUNCTION_ID_TAG, appFunctionMetadata.id)
128             )
129 
130             val schemaName = appFunctionMetadata.schemaName
131             val schemaCategory = appFunctionMetadata.schemaCategory
132             val schemaVersion = appFunctionMetadata.schemaVersion
133             if (schemaName != null && schemaCategory != null && schemaVersion != null) {
134                 appendChild(
135                     createElementWithTextNode(
136                         XmlElement.APP_FUNCTION_SCHEMA_CATEGORY_TAG,
137                         schemaCategory,
138                     )
139                 )
140                 appendChild(
141                     createElementWithTextNode(
142                         XmlElement.APP_FUNCTION_SCHEMA_NAME_TAG,
143                         schemaName,
144                     )
145                 )
146                 appendChild(
147                     createElementWithTextNode(
148                         XmlElement.APP_FUNCTION_SCHEMA_VERSION_TAG,
149                         schemaVersion.toString(),
150                     )
151                 )
152             }
153             appendChild(
154                 createElementWithTextNode(
155                     XmlElement.APP_FUNCTION_ENABLE_BY_DEFAULT_TAG,
156                     appFunctionMetadata.isEnabledByDefault.toString(),
157                 )
158             )
159         }
160 
161     private fun Document.createElementWithTextNode(elementName: String, text: String): Element =
162         createElement(elementName).apply { appendChild(createTextNode(text)) }
163 
164     private companion object {
165         private const val XML_PACKAGE_NAME = "assets"
166         private const val XML_FILE_NAME = "app_functions"
167         private const val XML_EXTENSION = "xml"
168 
169         private object XmlElement {
170             const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions"
171             const val APP_FUNCTION_ITEM_TAG = "appfunction"
172             const val APP_FUNCTION_ID_TAG = "function_id"
173             const val APP_FUNCTION_SCHEMA_CATEGORY_TAG = "schema_category"
174             const val APP_FUNCTION_SCHEMA_NAME_TAG = "schema_name"
175             const val APP_FUNCTION_SCHEMA_VERSION_TAG = "schema_version"
176             const val APP_FUNCTION_ENABLE_BY_DEFAULT_TAG = "enabled_by_default"
177         }
178     }
179 }
180