Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package datadog.gradle.plugin.config

import org.gradle.api.DefaultTask
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: DO you think it would be possible to have a task that handle both formats ? Maybe with a field that indicate the version of the file ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bric3 It's possible, but the formats of the files are so different that there would be little code reuse. My goal was to create this V2 task first, and remove the original task after I migrate the metadata/supported-configurations.json file. WDYT about that approach?

private val objects: ObjectFactory
) : DefaultTask() {
@InputFile
@PathSensitive(PathSensitivity.NONE)
val jsonFile = objects.fileProperty()

@get:OutputDirectory
val destinationDirectory = objects.directoryProperty()

@Input
val className = objects.property(String::class.java)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: By the way you can use the kotlin extension methods that live in org.gradle.kotlin.dsl. This allow to write in a more kotlin way:

+import org.gradle.kotlin.dsl.property
Suggested change
val className = objects.property(String::class.java)
val className = objects.property<String>()


@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<String, Any?> = FileInputStream(input).use { inStream ->
mapper.readValue(inStream, object : TypeReference<Map<String, Any?>>() {})
}

// Fetch top-level keys of JSON file
@Suppress("UNCHECKED_CAST")
val supportedRaw = fileData["supportedConfigurations"] as Map<String, List<Map<String, Any?>>>
@Suppress("UNCHECKED_CAST")
val deprecated = (fileData["deprecations"] as? Map<String, String>) ?: emptyMap()

// Parse supportedConfigurations key to into a V2 format
val supported: Map<String, List<SupportedConfiguration>> = supportedRaw.mapValues { (_, configList) ->
configList.map { configMap ->
SupportedConfiguration(
configMap["version"] as? String,
configMap["type"] as? String,
configMap["default"] as? String,
(configMap["aliases"] as? List<String>) ?: emptyList(),
(configMap["propertyKeys"] as? List<String>) ?: 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<String, List<String>> = supported.mapValues { (_, configList) ->
configList.flatMap { it.aliases }.distinct()
}

val aliasMapping = mutableMapOf<String, String>()
for ((canonical, alist) in aliases) {
for (alias in alist) aliasMapping[alias] = canonical
}

val reversePropertyKeysMap: Map<String, String> = 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<String, List<SupportedConfiguration>>,
aliases: Map<String, List<String>>,
aliasMapping: Map<String, String>,
deprecated: Map<String, String>,
reversePropertyKeysMap: Map<String, String>
) {
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<String, List<SupportedConfiguration>> SUPPORTED;")
out.println()
out.println(" public static final Map<String, List<String>> ALIASES;")
out.println()
out.println(" public static final Map<String, String> ALIAS_MAPPING;")
out.println()
out.println(" public static final Map<String, String> DEPRECATED;")
out.println()
out.println(" public static final Map<String, String> REVERSE_PROPERTY_KEYS_MAP;")
out.println()
out.println(" static {")
out.println()

// SUPPORTED
out.println(" Map<String, List<SupportedConfiguration>> 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(")));\n")
}
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<String, List<String>> 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<String, String> 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<String, String> 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<String, String> 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>): 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)}\""
}

data class SupportedConfiguration(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Id' rename this one as it is confusing with the outer class with the same name.

val version: String?,
val type: String?,
val default: String?,
val aliases: List<String>,
val propertyKeys: List<String>
)
Original file line number Diff line number Diff line change
@@ -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<String> aliases;
private final List<String> propertyKeys;

public SupportedConfiguration(
String version,
String type,
String defaultValue,
List<String> aliases,
List<String> 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<String> aliases() {
return aliases;
}

public List<String> propertyKeys() {
return propertyKeys;
}
}