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