All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.pixee.maven.operator.Util.kt Maven / Gradle / Ivy

@file:Suppress("DEPRECATION")

package io.github.pixee.maven.operator

import com.github.zafarkhaja.semver.Version
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.SystemUtils
import org.apache.commons.lang3.text.StrSubstitutor
import org.dom4j.Element
import org.dom4j.Node
import org.dom4j.Text
import org.dom4j.tree.DefaultText
import org.jaxen.SimpleNamespaceContext
import org.jaxen.XPath
import org.jaxen.dom4j.Dom4jXPath
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File


/**
 * Common Utilities
 */
object Util {
    private val LOGGER: Logger = LoggerFactory.getLogger(Util::class.java)

    /**
     * Extension Method that easily allows to add an element inside another while
     * retaining formatting
     *
     * @param d ProjectModel / Context
     * @param name new element ("tag") name
     * @return created element inside `this` object, already indented after and (optionally) before
     */
    fun Element.addIndentedElement(d: POMDocument, name: String): Element {
        val contentList = this.content()

        val indentLevel = findIndentLevel(this)

        val prefix = d.endl + StringUtils.repeat(d.indent, 1 + indentLevel)

        val suffix = d.endl + StringUtils.repeat(d.indent, indentLevel)

        if (contentList.isNotEmpty() && contentList.last() is Text) {
            val lastElement = contentList.last() as Text

            if (StringUtils.isWhitespace(lastElement.text)) {
                contentList.remove(contentList.last())
            }
        }

        contentList.add(DefaultText(prefix))

        val newElement = this.addElement(name)

        contentList.add(DefaultText(suffix))

        d.dirty = true

        return newElement
    }

    /**
     * Guesses the current indent level of the nearest nodes
     *
     * @return indent level
     */
    private fun findIndentLevel(startingNode: Element): Int {
        var level = 0

        var node = startingNode

        while (node.parent != null) {
            level += 1
            node = node.parent
        }

        return level
    }

    /**
     * Represents a Property Reference - as a regex
     */
    private val PROPERTY_REFERENCE_REGEX = Regex("^\\\$\\{(.*)}$")

    /**
     * Upserts a given property
     */
    private fun upgradeProperty(c: ProjectModel, d: POMDocument, propertyName: String) {
        if (null == d.resultPom.rootElement.element("properties")) {
            d.resultPom.rootElement.addIndentedElement(d, "properties")
        }

        val parentPropertyElement = d.resultPom.rootElement.element("properties")

        if (null == parentPropertyElement.element(propertyName)) {
            parentPropertyElement.addIndentedElement(d, propertyName)
        } else {
            if (!c.overrideIfAlreadyExists) {
                val propertyReferenceRE = Regex.fromLiteral("\${$propertyName}")

                val numberOfAllCurrentMatches =
                    propertyReferenceRE.findAll(d.resultPom.asXML()).toList().size

                if (numberOfAllCurrentMatches > 1) {
                    throw IllegalStateException("Property $propertyName is already defined - and used more than once.")
                }
            }
        }

        val propertyElement = parentPropertyElement.element(propertyName)

        if (!(propertyElement.text ?: "").trim().equals(c.dependency!!.version)) {
            propertyElement.text = c.dependency!!.version

            d.dirty = true
        }
    }

    /**
     * Creates a property Name
     */
    internal fun propertyName(c: ProjectModel, versionNode: Element): String {
        val version = versionNode.textTrim

        if (PROPERTY_REFERENCE_REGEX.matches(version)) {
            val match = PROPERTY_REFERENCE_REGEX.find(version)

            val firstMatch = match!!.groups[1]!!

            return firstMatch.value
        }

        return "versions." + c.dependency!!.artifactId
    }

    /**
     * Identifies if an upgrade is needed
     */
    internal fun findOutIfUpgradeIsNeeded(c: ProjectModel, versionNode: Element): Boolean {
        val currentVersionNodeText = resolveVersion(c, versionNode.text!!)

        val currentVersion = Version.valueOf(currentVersionNodeText)
        val newVersion = Version.valueOf(c.dependency!!.version)

        @Suppress("UnnecessaryVariable") val versionsAreIncreasing =
            newVersion.greaterThan(currentVersion)

        return versionsAreIncreasing
    }

    private fun resolveVersion(c: ProjectModel, versionText: String): String =
        if (PROPERTY_REFERENCE_REGEX.matches(versionText)) {
            @Suppress("DEPRECATION")
            StrSubstitutor(c.resolvedProperties).replace(versionText)
        } else {
            versionText
        }

    /**
     * Escapes a Property Name
     */
    private fun escapedPropertyName(propertyName: String): String =
        "\${$propertyName}"

    /**
     * Given a Version Node, upgrades a resulting POM
     */
    internal fun upgradeVersionNode(
        c: ProjectModel,
        versionNode: Element,
        pomDocumentHoldingProperty: POMDocument
    ) {
        if (c.useProperties) {
            val propertyName = propertyName(c, versionNode)

            // define property
            upgradeProperty(c, pomDocumentHoldingProperty, propertyName)

            versionNode.text = escapedPropertyName(propertyName)
        } else {
            if (!(versionNode.text ?: "").trim().equals(c.dependency!!.version)) {
                pomDocumentHoldingProperty.dirty = true
                versionNode.text = c.dependency!!.version
            }
        }
    }

    /**
     * Builds a Lookup Expression String for a given dependency
     *
     * @param dependency Dependency
     */
    fun buildLookupExpressionForDependency(dependency: Dependency): String =
        "/m:project" +
                "/m:dependencies" +
                "/m:dependency" +
                /* */ "[./m:groupId[text()='${dependency.groupId}'] and " +
                /*  */ "./m:artifactId[text()='${dependency.artifactId}']" +
                "]"

    /**
     * Builds a Lookup Expression String for a given dependency, but under the >dependencyManagement> section
     *
     * @param dependency Dependency
     */
    fun buildLookupExpressionForDependencyManagement(dependency: Dependency): String =
        "/m:project" +
                "/m:dependencyManagement" +
                "/m:dependencies" +
                "/m:dependency" +
                /* */ "[./m:groupId[text()='${dependency.groupId}'] and " +
                /*  */ "./m:artifactId[text()='${dependency.artifactId}']" +
                "]"

    /**
     * Extension Function to Select the XPath Nodes
     *
     * @param expression expression to use
     */
    @Suppress("UNCHECKED_CAST")
    fun Node.selectXPathNodes(expression: String) =
        createXPathExpression(expression).selectNodes(this)!! as List

    /**
     * Creates a XPath Expression from a given expression string
     *
     * @param expression expression to create xpath from
     */
    private fun createXPathExpression(expression: String): XPath {
        val xpath = Dom4jXPath(expression)

        xpath.namespaceContext = namespaceContext

        return xpath
    }

    /**
     * Hard-Coded POM Namespace Map
     */
    private val namespaceContext = SimpleNamespaceContext(
        mapOf(
            "m" to "http://maven.apache.org/POM/4.0.0"
        )
    )


    internal fun which(path: String): File? {
        val nativeExecutables: List = if (SystemUtils.IS_OS_WINDOWS) {
            listOf("", ".exe", ".bat", ".cmd").map { path + it }.toList()
        } else {
            listOf(path)
        }

        val pathContentString = System.getenv("PATH")

        val pathElements = pathContentString.split(File.pathSeparatorChar)

        val possiblePaths = nativeExecutables.flatMap { executable ->
            pathElements.map { pathElement ->
                File(File(pathElement), executable)
            }
        }

        val isCliCallable: (File) -> Boolean = if (SystemUtils.IS_OS_WINDOWS) { it ->
            it.exists() && it.isFile
        } else { it ->
            it.exists() && it.isFile && it.canExecute()
        }

        val result = possiblePaths.findLast(isCliCallable)

        if (null == result) {
            LOGGER.warn(
                "Unable to find mvn executable (execs: {}, path: {})",
                nativeExecutables.joinToString("/"),
                pathContentString
            )
        }

        return result
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy