• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.config
18 
19 import com.fasterxml.jackson.annotation.JsonInclude
20 import com.fasterxml.jackson.databind.SerializationFeature
21 import com.fasterxml.jackson.dataformat.xml.XmlMapper
22 import com.fasterxml.jackson.module.kotlin.kotlinModule
23 import java.io.File
24 import javax.xml.XMLConstants
25 import javax.xml.parsers.SAXParserFactory
26 import javax.xml.validation.SchemaFactory
27 import org.xml.sax.SAXParseException
28 import org.xml.sax.helpers.DefaultHandler
29 
30 const val CONFIG_NAMESPACE = "http://www.google.com/tools/metalava/config"
31 
32 /** Parser for XML configuration files. */
33 class ConfigParser private constructor() : DefaultHandler() {
34     /** Errors that were reported while parsing a configuration file. */
35     private val errors = StringBuilder()
36 
37     private fun recordException(path: String, message: String) {
38         errors.apply {
39             append("    ")
40             append(path)
41             append(": ")
42             append(message)
43             append("\n")
44         }
45     }
46 
47     private fun recordParseException(exception: SAXParseException) {
48         errors.apply {
49             append("    ")
50             append(exception.systemId)
51             append(":")
52             append(exception.lineNumber)
53             append(": ")
54             append(exception.message)
55             append("\n")
56         }
57     }
58 
59     override fun warning(exception: SAXParseException) {
60         recordParseException(exception)
61     }
62 
63     override fun error(exception: SAXParseException) {
64         recordParseException(exception)
65     }
66 
67     companion object {
68         /** Parse a list of configuration files in order, returning a single [Config] object. */
69         fun parse(files: List<File>): Config {
70             val schemaUrl = ConfigParser::class.java.getResource("/schemas/config.xsd")
71             val schemafactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
72             val schema = schemafactory.newSchema(schemaUrl)
73 
74             val saxParserFactory = SAXParserFactory.newNSInstance()
75             saxParserFactory.schema = schema
76             val saxParser = saxParserFactory.newSAXParser()
77             val configParser = ConfigParser()
78             val xmlMapper = configXmlMapper()
79 
80             // Parse all the configuration files, validating against the schema, collating any
81             // errors that are reported.
82             for (file in files) {
83                 // Parse the configuration file to validate against the schema first.
84                 try {
85                     saxParser.parse(file, configParser)
86                 } catch (e: SAXParseException) {
87                     configParser.recordParseException(e)
88                 } catch (e: Exception) {
89                     configParser.recordException(file.path, e.message ?: "")
90                 }
91             }
92 
93             // If any errors were reported then fail as it is unlikely that reading or using the
94             // configuration file will work.
95             if (configParser.errors.isNotEmpty()) {
96                 error("Errors found while parsing configuration file(s):\n${configParser.errors}")
97             }
98 
99             return files
100                 .map { file ->
101                     // Read the configuration file into a Config object.
102                     xmlMapper.readValue(file, Config::class.java)
103                 }
104                 // Merge the config objects together.
105                 .reduceOrNull(Config::combineWith)
106                 // Validate the config.
107                 ?.apply { validate() }
108             // If no configuration files were created then return an empty Config.
109             ?: Config()
110         }
111 
112         /**
113          * Get an [XmlMapper] that can be used to serialize and deserialize [Config] objects.
114          *
115          * While serializing a [Config] object is not something that is used by Metalava it is
116          * helpful to be able to do that for debugging and also for development. e.g. it is easy to
117          * work out what the [XmlMapper] can read by simply seeing what it writes out as it
118          * generally supports reading what it writes. Tweaking it to match what is defined in the
119          * schema just requires adding the correct annotations to the object.
120          */
121         internal fun configXmlMapper(): XmlMapper {
122             return XmlMapper.builder()
123                 // Do not add extra wrapper elements around collections.
124                 .defaultUseWrapper(false)
125                 // Pretty print, indenting each level by 2 spaces.
126                 .enable(SerializationFeature.INDENT_OUTPUT)
127                 // Exclude any `null` values from being serialized.
128                 .serializationInclusion(JsonInclude.Include.NON_NULL)
129                 // Add support for using Kotlin data classes.
130                 .addModule(kotlinModule())
131                 .build()
132         }
133     }
134 }
135