Check for newer versions of dependencies in pom.xml
Tags: maven, groovy
Published: 2024-05-08 22:59:30

---
title: Check for newer versions of dependencies in pom.xml
published: true
description: 
tags: maven, groovy
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-05-08 18:54 +0000
---

I made a script in Groovy language to look for newer versions of all declared dependencies in a Maven project. The script supports multi-module projects.

It searches the current folder for pom.xml files and checks the central.sonatype.com website for a newer versions. Only dependencies that have an explicit version defined in pom will be checked. Versions defined by properties will be resolved if the properties were declared in one of the pom files. (If a property is defined more than once, the result is undefined.)

Example of use:

![check-versions.sh example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/95e2w8fgurxn2p3e5bk5.png)

> I recommend installing Java 17 (or newer) and Groovy 4 with sdkman!

Put these two files in a folder that is included in PATH.

`check-versions.sh`:
```sh
#!/usr/bin/env bash
BASEDIR=$(unset CDPATH; cd `dirname $0`; pwd)

JAVA_VER=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F '.' '{sub("^$", "0", $2); print $1$2}')
if [ "$JAVA_VER" -lt 170 ]; then
    echo "Java 17 necessario"
    exit 1
fi

#$PROXY="-DproxySet=true -DproxyHost=10.0.0.24 -DproxyPort=8080"

groovy -Dgroovy.grape.report.downloads=true $PROXY $BASEDIR/check-versions.groovy 2>/dev/null
```

`check-versions.groovy`:
```groovy
import groovy.json.JsonSlurper
import groovy.xml.XmlParser
@Grab('org.jodd:jodd-lagarto:6.0.6')
import jodd.jerry.Jerry
@Grab('org.fusesource.jansi:jansi:2.4.1')
import org.fusesource.jansi.AnsiConsole
@Grab('org.slf4j:slf4j-jdk-platform-logging:2.0.13')
import org.slf4j.Logger

import static org.fusesource.jansi.Ansi.Color.GREEN
import static org.fusesource.jansi.Ansi.Color.RED
import static org.fusesource.jansi.Ansi.ansi

AnsiConsole.systemInstall()

record Dependency(String groupId, String artifactId, String version) {}

static def findAllDependencies() {
    //println("-- findAllDependencies")
    def allDeps = []
    def mvnProperties = [:]
    new File(".").traverse(type: groovy.io.FileType.FILES, nameFilter: ~/pom.xml/) {
        processPom(new XmlParser().parse(it), allDeps, mvnProperties)
    }
    return allDeps.unique().sort { k1, k2 -> k1.groupId + k1.artifactId <=> k2.groupId + k2.artifactId }
}

static def processPom(def project, def allDeps, def mvnProperties) {
    collectProperties(project, mvnProperties)
    collectDependencies(project.'**'.dependencies.dependency, allDeps, mvnProperties)
    collectDependencies(project.parent, allDeps, mvnProperties)
    collectDependencies(project.'**'.plugin, allDeps, mvnProperties)
}

static def collectProperties(def project, def mvnProperties) {
    //println("-- collectProperties")
    for (def p : project.properties) {
        for (def c : p.children()) {
            def name = c.name().localPart
            def value = c.text()
            mvnProperties[name] = value
        }
    }
}

static def collectDependencies(def dependencies, def allDeps, def mvnProperties) {
    //println("-- collectDependencies")
    for (def d : dependencies) {
        def groupId = d.groupId.text()
        if (groupId.isEmpty()) {
            // assume default groupId for maven plugins
            groupId = "org.apache.maven.plugins"
        }
        def artifactId = d.artifactId.text()
        def version = d.version.text()
        if (version.isEmpty())
            continue
        if (version.startsWith("\$")) {
            def matcher = (version =~ /\$\{(.+?)\}/)
            if (matcher.find()) {
                def property = matcher.group(1)
                version = mvnProperties[property]
                if (version == null) {
                    continue
                }
            }
        }
        def x = new Dependency(groupId, artifactId, version)
        allDeps.add(x)
    }
}

/**
 * This method is highly dependable on the html format returned by central.sonatype.com
 *
 * Return this map:
 * [name:logstash-logback-encoder, namespace:net.logstash.logback, version:7.4, versions:[7.4, 7.3, 7.2, 7.1.1, 7.1, 7.0.1, 7.0, 6.6, 6.5, 6.4, 6.3, 6.2, 6.1, 6.0, 5.3, 5.2, 5.1, 5.0, 4.11, 4.10], tags:[], usedIn:1600, tab:versions]
 */
def findLatestVersion(String groupId, String artifactId) {
    //println("-- findLatestVersion ${groupId}:${artifactId}")
    def url = "https://central.sonatype.com/artifact/${groupId}/${artifactId}/versions"
    def body = url.toURL().getText()
    def doc = Jerry.of(body)
    def scripts = doc.find('html.nx-html.nx-html--page-scrolling body.nx-body script')

    def text = ""
    scripts.each {
        def element ->
            if (element.text().contains('self.__next_f.push')) {
                text = element.text()
            }
    }

    int i = text.indexOf(':')
    int j = text.lastIndexOf('"')
    text = text.substring(i + 1, j - 2)
    text = text.replaceAll('\\\\"', '"')
    text = text.replaceAll('\\\\"', '"')

    def json = new JsonSlurper().parseText(text)
    try {
        def result = json[0][3].children[0][3]
        return result
    }
    catch (Exception ignored) {
        return null
    }
}

//skipVersions = [:]
skipVersions = [
        "org.springframework.boot:spring-boot-starter-parent"  : "3.2",
        "org.springframework.boot:spring-boot-starter-data-jpa": "3.2",
        "org.springframework.boot:spring-boot-starter-web"     : "3.2",
        "org.springframework.boot:spring-boot-dependencies"    : "3.2",
        "org.springframework.cloud:spring-cloud-dependencies"  : "2023",
]

def getLatestVersion(String groupId, String artifactId) {
    //println("-- getLatestVersion")
    def versions = findLatestVersion(groupId, artifactId)
    def skip = skipVersions["${groupId}:${artifactId}"]
    if (skip == null) {
        return versions?.version
    }

    for (def v in versions.versions) {
        if (v.startsWith(skip)) {
            continue
        }
        return v
    }
}

def allDeps = findAllDependencies()

for (def d : allDeps) {
    def latest = getLatestVersion(d.groupId, d.artifactId)
    if (latest == null) {
        continue
    }

    print("${d.groupId}:${d.artifactId}:${d.version} ")

    if (d.version.equals(latest)) {
        println("${ansi().fg(GREEN)}[OK]${ansi().reset()}")
    } else {
        println("${ansi().fg(RED)}[${latest}]${ansi().reset()}")
    }
}
```

By changing the `skipVersions` variable, it is possible to ignore a version of some dependencies.

For example: I'm stuck with Spring Boot 3.1, so the config below will ignore version 3.2, but will still notify for newer 3.1.x version.

```groovy
skipVersions = [
        "org.springframework.boot:spring-boot-starter-parent"  : "3.2",
        "org.springframework.boot:spring-boot-starter-data-jpa": "3.2",
        "org.springframework.boot:spring-boot-starter-web"     : "3.2",
        "org.springframework.boot:spring-boot-dependencies"    : "3.2",
        "org.springframework.cloud:spring-cloud-dependencies"  : "2023",
]
```

If there is no dependency to skip, just use an empty map:
```groovy
skipVersions = [:]
```