diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt new file mode 100644 index 00000000000..56082bc95c8 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -0,0 +1,215 @@ +package datadog.gradle.plugin.config + +import org.gradle.api.DefaultTask +import org.gradle.kotlin.dsl.property +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import java.io.File +import java.io.FileInputStream +import java.io.PrintWriter +import javax.inject.Inject + +@CacheableTask +abstract class ParseV2SupportedConfigurationsTask @Inject constructor( + private val objects: ObjectFactory +) : DefaultTask() { + @InputFile + @PathSensitive(PathSensitivity.NONE) + val jsonFile = objects.fileProperty() + + @get:OutputDirectory + val destinationDirectory = objects.directoryProperty() + + @Input + val className = objects.property() + + @TaskAction + fun generate() { + val input = jsonFile.get().asFile + val outputDir = destinationDirectory.get().asFile + val finalClassName = className.get() + outputDir.mkdirs() + + // Read JSON (directly from the file, not classpath) + val mapper = ObjectMapper() + val fileData: Map = FileInputStream(input).use { inStream -> + mapper.readValue(inStream, object : TypeReference>() {}) + } + + // Fetch top-level keys of JSON file + @Suppress("UNCHECKED_CAST") + val supportedRaw = fileData["supportedConfigurations"] as Map>> + @Suppress("UNCHECKED_CAST") + val deprecated = (fileData["deprecations"] as? Map) ?: emptyMap() + + // Parse supportedConfigurations key to into a V2 format + val supported: Map> = supportedRaw.mapValues { (_, configList) -> + configList.map { configMap -> + SupportedConfigurationItem( + configMap["version"] as? String, + configMap["type"] as? String, + configMap["default"] as? String, + (configMap["aliases"] as? List) ?: emptyList(), + (configMap["propertyKeys"] as? List) ?: emptyList() + ) + } + } + + // Generate top-level mapping from config -> list of aliases and reverse alias mapping from alias -> top-level config + // Note: This top-level alias mapping will be deprecated once Config Registry is mature enough to understand which version of a config a customer is using + val aliases: Map> = supported.mapValues { (_, configList) -> + configList.flatMap { it.aliases }.distinct() + } + + val aliasMapping = mutableMapOf() + for ((canonical, alist) in aliases) { + for (alias in alist) aliasMapping[alias] = canonical + } + + val reversePropertyKeysMap: Map = supported.flatMap { (canonical, configList) -> + configList.flatMap { config -> + config.propertyKeys.map { propertyKey -> propertyKey to canonical } + } + }.toMap() + + // Build the output .java path from the fully-qualified class name + val pkgName = finalClassName.substringBeforeLast('.', "") + val pkgPath = pkgName.replace('.', File.separatorChar) + val simpleName = finalClassName.substringAfterLast('.') + val pkgDir = if (pkgPath.isEmpty()) outputDir else File(outputDir, pkgPath).also { it.mkdirs() } + val generatedFile = File(pkgDir, "$simpleName.java").absolutePath + + // Call your existing generator (same signature as in your Java code) + generateJavaFile( + generatedFile, + simpleName, + pkgName, + supported, + aliases, + aliasMapping, + deprecated, + reversePropertyKeysMap + ) + } + + private fun generateJavaFile( + outputPath: String, + className: String, + packageName: String, + supported: Map>, + aliases: Map>, + aliasMapping: Map, + deprecated: Map, + reversePropertyKeysMap: Map + ) { + val outFile = File(outputPath) + outFile.parentFile?.mkdirs() + + PrintWriter(outFile).use { out -> + out.println("package $packageName;") + out.println() + out.println("import java.util.*;") + out.println() + out.println("public final class $className {") + out.println() + out.println(" public static final Map> SUPPORTED;") + out.println() + out.println(" public static final Map> ALIASES;") + out.println() + out.println(" public static final Map ALIAS_MAPPING;") + out.println() + out.println(" public static final Map DEPRECATED;") + out.println() + out.println(" public static final Map REVERSE_PROPERTY_KEYS_MAP;") + out.println() + out.println(" static {") + out.println() + + // SUPPORTED + out.println(" Map> supportedMap = new HashMap<>();") + for ((key, configList) in supported.toSortedMap()) { + out.print(" supportedMap.put(\"${esc(key)}\", Collections.unmodifiableList(Arrays.asList(") + val configIter = configList.iterator() + while (configIter.hasNext()) { + val config = configIter.next() + out.print("new SupportedConfiguration(") + out.print("${escNullableString(config.version)}, ") + out.print("${escNullableString(config.type)}, ") + out.print("${escNullableString(config.default)}, ") + out.print("Arrays.asList(${quoteList(config.aliases)}), ") + out.print("Arrays.asList(${quoteList(config.propertyKeys)})") + out.print(")") + if (configIter.hasNext()) out.print(", ") + } + out.println(")));") + } + out.println(" SUPPORTED = Collections.unmodifiableMap(supportedMap);") + out.println() + + // ALIASES + out.println(" // Note: This top-level alias mapping will be deprecated once Config Registry is mature enough to understand which version of a config a customer is using") + out.println(" Map> aliasesMap = new HashMap<>();") + for ((canonical, list) in aliases.toSortedMap()) { + out.printf( + " aliasesMap.put(\"%s\", Collections.unmodifiableList(Arrays.asList(%s)));\n", + esc(canonical), + quoteList(list) + ) + } + out.println(" ALIASES = Collections.unmodifiableMap(aliasesMap);") + out.println() + + // ALIAS_MAPPING + out.println(" Map aliasMappingMap = new HashMap<>();") + for ((alias, target) in aliasMapping.toSortedMap()) { + out.printf(" aliasMappingMap.put(\"%s\", \"%s\");\n", esc(alias), esc(target)) + } + out.println(" ALIAS_MAPPING = Collections.unmodifiableMap(aliasMappingMap);") + out.println() + + // DEPRECATED + out.println(" Map deprecatedMap = new HashMap<>();") + for ((oldKey, note) in deprecated.toSortedMap()) { + out.printf(" deprecatedMap.put(\"%s\", \"%s\");\n", esc(oldKey), esc(note)) + } + out.println(" DEPRECATED = Collections.unmodifiableMap(deprecatedMap);") + out.println() + + // REVERSE_PROPERTY_KEYS_MAP + out.println(" Map reversePropertyKeysMapping = new HashMap<>();") + for ((propertyKey, config) in reversePropertyKeysMap.toSortedMap()) { + out.printf(" reversePropertyKeysMapping.put(\"%s\", \"%s\");\n", esc(propertyKey), esc(config)) + } + out.println(" REVERSE_PROPERTY_KEYS_MAP = Collections.unmodifiableMap(reversePropertyKeysMapping);") + out.println() + + out.println(" }") + out.println("}") + } + } + + private fun quoteList(list: List): String = + list.joinToString(", ") { "\"${esc(it)}\"" } + + private fun esc(s: String): String = + s.replace("\\", "\\\\").replace("\"", "\\\"") + + private fun escNullableString(s: String?): String = + if (s == null) "null" else "\"${esc(s)}\"" +} + +private data class SupportedConfigurationItem( + val version: String?, + val type: String?, + val default: String?, + val aliases: List, + val propertyKeys: List +) diff --git a/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfiguration.java b/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfiguration.java new file mode 100644 index 00000000000..dfee6583d55 --- /dev/null +++ b/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfiguration.java @@ -0,0 +1,44 @@ +package datadog.trace.config.inversion; + +import java.util.List; + +public class SupportedConfiguration { + private final String version; + private final String type; + private final String defaultValue; + private final List aliases; + private final List propertyKeys; + + public SupportedConfiguration( + String version, + String type, + String defaultValue, + List aliases, + List propertyKeys) { + this.version = version; + this.type = type; + this.defaultValue = defaultValue; + this.aliases = aliases; + this.propertyKeys = propertyKeys; + } + + public String version() { + return version; + } + + public String type() { + return type; + } + + public String defaultValue() { + return defaultValue; + } + + public List aliases() { + return aliases; + } + + public List propertyKeys() { + return propertyKeys; + } +}