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

nl.gn0s1s.jackson.versioncheck.JacksonVersionCheckPlugin.scala Maven / Gradle / Ivy

The newest version!
package nl.gn0s1s.jackson.versioncheck

import sbt.*
import sbt.Keys.*

object JacksonVersionCheckPlugin extends AutoPlugin {
  override def trigger = allRequirements

  object autoImport {
    lazy val jacksonVersionCheckFailBuildOnNonMatchingVersions =
      settingKey[Boolean]("Sets whether non-matching Jackson module versions fail the build")
    lazy val jacksonVersionCheckStrict                         =
      settingKey[Boolean]("Sets whether Jackson modules versions should match exactly, including the patch version.")
    val jacksonVersionCheck                                    =
      taskKey[Unit]("Check that all Jackson modules have the same version")
  }

  import autoImport.*

  override lazy val globalSettings: Seq[Def.Setting[Boolean]] = Seq(
    jacksonVersionCheckFailBuildOnNonMatchingVersions := false,
    jacksonVersionCheckStrict                         := false
  )

  override lazy val projectSettings: Seq[Def.Setting[Task[Unit]]] = Seq(
    jacksonVersionCheck := checkModuleVersions(
      updateFull.value,
      streams.value.log,
      jacksonVersionCheckFailBuildOnNonMatchingVersions.value,
      jacksonVersionCheckStrict.value
    )
  )

  private val jacksonModules = Set(
    "jackson-annotations",
    "jackson-core",
    "jackson-databind",
    "jackson-dataformat-avro",
    "jackson-dataformat-cbor",
    "jackson-dataformat-csv",
    "jackson-dataformat-ion",
    "jackson-dataformat-properties",
    "jackson-dataformat-protobuf",
    "jackson-dataformat-smile",
    "jackson-dataformat-toml",
    "jackson-dataformat-xml",
    "jackson-dataformat-yaml",
    "jackson-dataformats-binary",
    "jackson-dataformats-text",
    "jackson-datatype-eclipse-collections",
    "jackson-datatype-guava",
    "jackson-datatype-hibernate",
    "jackson-datatype-hibernate-parent",
    "jackson-datatype-hibernate3",
    "jackson-datatype-hibernate4",
    "jackson-datatype-hibernate5",
    "jackson-datatype-hibernate5-jakarta",
    "jackson-datatype-hibernate6",
    "jackson-datatype-hppc",
    "jackson-datatype-jakarta-jsonp",
    "jackson-datatype-jakarta-mail",
    "jackson-datatype-jaxrs",
    "jackson-datatype-jdk7",
    "jackson-datatype-jdk8",
    "jackson-datatype-joda",
    "jackson-datatype-joda-money",
    "jackson-datatype-json-org",
    "jackson-datatype-jsr310",
    "jackson-datatype-jsr353",
    "jackson-datatype-pcollections",
    "jackson-datatypes-collections",
    "jackson-datatypes-misc-parent",
    "jackson-module-afterburner",
    "jackson-module-android-record",
    "jackson-module-blackbird",
    "jackson-module-guice",
    "jackson-module-guice7",
    "jackson-module-jakarta-xmlbind-annotations",
    "jackson-module-jaxb-annotations",
    "jackson-module-jsonSchema",
    "jackson-module-jsonSchema-jakarta",
    "jackson-module-jsonSchema-parent",
    "jackson-module-kotlin",
    "jackson-module-mrbean",
    "jackson-module-no-ctor-deser",
    "jackson-module-osgi",
    "jackson-module-parameter-names",
    "jackson-module-paranamer",
    "jackson-module-scala",
    "jackson-modules-base",
    "jackson-modules-java8"
  )

  private sealed trait Group

  private case object JacksonModule extends Group

  private case object Others extends Group

  private def moduleNameWithoutScalaVersion(m: ModuleID): String =
    m.name.replaceFirst("(_2\\.\\d\\d|_3)$", "")

  private def checkModuleVersions(
      updateReport: UpdateReport,
      log: Logger,
      failBuildOnNonMatchingVersions: Boolean,
      strict: Boolean
  ): Unit = {
    log.info("Checking Jackson module versions")
    val allModules = updateReport.allModules
    val grouped    = allModules
      .filter(m =>
        m.organization == "com.fasterxml.jackson.core" ||
          m.organization == "com.fasterxml.jackson.dataformat" ||
          m.organization == "com.fasterxml.jackson.datatype" ||
          m.organization == "com.fasterxml.jackson.module"
      )
      .groupBy { m =>
        if (jacksonModules(moduleNameWithoutScalaVersion(m))) JacksonModule
        else Others
      }
    val jacksonOk  =
      grouped.get(JacksonModule).forall(verifyVersions(
        _,
        log,
        failBuildOnNonMatchingVersions,
        strict
      ))

    for {
      jacksonDatabindVersion    <-
        allModules.filter(_.name == "jackson-databind").map(m => Version(m.revision)).sorted.lastOption
      jacksonModuleScalaVersion <-
        allModules.filter(m => moduleNameWithoutScalaVersion(m) == "jackson-module-scala").map(m => Version(m.revision))
          .sorted.lastOption
    } yield verifyJacksonModuleScalaRequirement(jacksonDatabindVersion, jacksonModuleScalaVersion, log)

    if (failBuildOnNonMatchingVersions && !jacksonOk)
      throw NonMatchingVersionsException
  }

  private def extractMajorMinor(version: String): (Int, Int) =
    version.split('.') match {
      case Array(major, minor, _*) => (major.toInt, minor.toInt)
      case _                       => (0, 0)
    }

  private def verifyVersions(
      modules: Seq[ModuleID],
      log: Logger,
      failBuildOnNonMatchingVersions: Boolean,
      strict: Boolean
  ): Boolean = {
    val modulesLatestRevision = modules.maxBy(m => Version(m.revision)).revision
    val modulesTobeUpdated    =
      modules.collect {
        case m
            if (strict && m.revision != modulesLatestRevision) || (!strict && extractMajorMinor(
              m.revision
            ) != extractMajorMinor(modulesLatestRevision)) => moduleNameWithoutScalaVersion(m)
      }.sorted
    if (modulesTobeUpdated.nonEmpty) {
      val groupedByVersion = modules
        .groupBy(_.revision)
        .toSeq
        .sortBy(r => Version(r._1))
        .map { case (k, v) => k -> v.map(moduleNameWithoutScalaVersion).sorted.mkString("[", ", ", "]") }
        .map { case (k, v) => s"($k, $v)" }
        .mkString(", ")
      val report           = s"You are using version $modulesLatestRevision of Jackson, but it appears " +
        s"you (perhaps indirectly) also depend on older versions of related modules. " +
        s"You can solve this by adding an explicit dependency on version $modulesLatestRevision " +
        s"of the [${modulesTobeUpdated.mkString(", ")}] modules to your project. " +
        s"Here's a complete collection of detected modules: $groupedByVersion."

      if (failBuildOnNonMatchingVersions)
        log.error(report)
      else
        log.warn(report)
    }
    modulesTobeUpdated.isEmpty
  }

  private def verifyJacksonModuleScalaRequirement(
      jacksonDatabindVersion: Version,
      jacksonModuleScalaVersion: Version,
      log: Logger
  ): Unit = {
    val (databindMajor, databindMinor)       = extractMajorMinor(jacksonDatabindVersion.version)
    val (moduleScalaMajor, moduleScalaMinor) = extractMajorMinor(jacksonModuleScalaVersion.version)

    if (databindMajor != moduleScalaMajor || databindMinor != moduleScalaMinor)
      log.warn(
        s"Jackson Scala module ${jacksonModuleScalaVersion.version} requires Jackson Databind version >=" +
          s" $moduleScalaMajor.$moduleScalaMinor.0 and < $moduleScalaMajor.${moduleScalaMinor + 1}.0 - Found " +
          s"jackson-databind version ${jacksonDatabindVersion.version}"
      )
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy