PomBuildFileUpdater.kt

package com.depanalyzer.update

import com.depanalyzer.security.InputSafety
import org.xml.sax.InputSource
import java.io.File
import java.io.StringReader
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory

class PomBuildFileUpdater : BuildFileUpdater {
    override fun applyUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        if (!InputSafety.isSafeVersion(suggestion.newVersion)) return false

        return when (suggestion.targetType) {
            UpdateTargetType.DIRECT -> applyDirectUpdate(buildFile, suggestion)
            UpdateTargetType.TRANSITIVE_OVERRIDE -> applyTransitiveOverride(buildFile, suggestion)
        }
    }

    private fun applyDirectUpdate(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        val content = buildFile.readText()
        val dependencyRegex = Regex(
            """(<dependency>\s*<groupId>\Q${suggestion.groupId}\E</groupId>\s*<artifactId>\Q${suggestion.artifactId}\E</artifactId>[\s\S]*?<version>)([^<]+)(</version>)"""
        )

        val dependencyMatch = dependencyRegex.find(content) ?: return false
        val currentToken = dependencyMatch.groupValues[2].trim()

        val replaced = when {
            currentToken == suggestion.currentVersion -> {
                dependencyRegex.replace(content) { match ->
                    val current = match.groupValues[2].trim()
                    if (current == suggestion.currentVersion) {
                        "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
                    } else {
                        match.value
                    }
                }
            }

            currentToken.isPropertyReference() -> {
                val propertyName = currentToken.extractPropertyName() ?: return false
                replacePropertyValue(content, propertyName, suggestion)
            }

            else -> content
        }

        if (replaced == content) return false
        return writeIfValidXml(buildFile, content, replaced)
    }

    private fun applyTransitiveOverride(buildFile: File, suggestion: UpdateSuggestion): Boolean {
        val content = buildFile.readText()
        val existingDepRegex = Regex(
            """(<dependency>\s*<groupId>\Q${suggestion.groupId}\E</groupId>\s*<artifactId>\Q${suggestion.artifactId}\E</artifactId>[\s\S]*?<version>)([^<]+)(</version>)"""
        )

        val existingUpdated = existingDepRegex.replace(content) { match ->
            val current = match.groupValues[2].trim()
            if (current == suggestion.newVersion) match.value
            else "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
        }
        if (existingUpdated != content) {
            buildFile.writeText(existingUpdated)
            return true
        }

        val dependencySnippet = """
            <dependency>
                <groupId>${suggestion.groupId}</groupId>
                <artifactId>${suggestion.artifactId}</artifactId>
                <version>${suggestion.newVersion}</version>
            </dependency>
        """.trimIndent()

        val managementRegex =
            Regex("""(<dependencyManagement>\s*<dependencies>)([\s\S]*?)(</dependencies>\s*</dependencyManagement>)""")
        val withManagement = managementRegex.find(content)?.let { match ->
            val insertion = "\n$dependencySnippet\n"
            content.replaceRange(
                match.range,
                "${match.groupValues[1]}${match.groupValues[2]}$insertion${match.groupValues[3]}"
            )
        }

        val finalContent = when {
            withManagement != null -> withManagement
            content.contains("</project>") -> {
                val block = """
                    <dependencyManagement>
                        <dependencies>
                            $dependencySnippet
                        </dependencies>
                    </dependencyManagement>
                """.trimIndent()
                content.replace("</project>", "\n$block\n</project>")
            }

            else -> content
        }

        if (finalContent == content) return false
        return writeIfValidXml(buildFile, content, finalContent)
    }

    private fun writeIfValidXml(buildFile: File, originalContent: String, candidateContent: String): Boolean {
        if (candidateContent == originalContent) return false
        if (!isWellFormedXml(candidateContent)) return false
        buildFile.writeText(candidateContent)
        return true
    }

    private fun isWellFormedXml(content: String): Boolean {
        return try {
            val factory = DocumentBuilderFactory.newInstance()
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
            factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false)
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
            factory.isExpandEntityReferences = false
            factory.isXIncludeAware = false
            factory.newDocumentBuilder().parse(InputSource(StringReader(content)))
            true
        } catch (_: Exception) {
            false
        }
    }

    private fun replacePropertyValue(content: String, propertyName: String, suggestion: UpdateSuggestion): String {
        val propertyRegex = Regex("""(<\Q$propertyName\E>)([^<]+)(</\Q$propertyName\E>)""")
        val propertyMatch = propertyRegex.find(content) ?: return content
        val current = propertyMatch.groupValues[2].trim()

        if (current != suggestion.currentVersion) return content

        return propertyRegex.replace(content) { match ->
            if (match.groupValues[2].trim() == suggestion.currentVersion) {
                "${match.groupValues[1]}${suggestion.newVersion}${match.groupValues[3]}"
            } else {
                match.value
            }
        }
    }

    private fun String.isPropertyReference(): Boolean {
        return startsWith("\${") && endsWith("}")
    }

    private fun String.extractPropertyName(): String? {
        if (!isPropertyReference()) return null
        return removePrefix("\${").removeSuffix("}").trim()
    }
}