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

io.gatling.commons.util.GatlingVersion.scala Maven / Gradle / Ivy

/*
 * Copyright 2011-2023 GatlingCorp (https://gatling.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.gatling.commons.util

import java.net.{ HttpURLConnection, URI }
import java.nio.charset.StandardCharsets
import java.time.{ Instant, ZoneOffset, ZonedDateTime }
import java.time.format.DateTimeFormatter
import java.util.ResourceBundle
import java.util.prefs.Preferences

import scala.util.{ Success, Try }
import scala.util.control.NonFatal

import com.typesafe.scalalogging.StrictLogging

object GatlingVersion {
  val ThisVersion: GatlingVersion = {
    val bundle = ResourceBundle.getBundle("gatling-version")
    GatlingVersion(
      bundle.getString("version"),
      ZonedDateTime.parse(bundle.getString("release-date"), DateTimeFormatter.ISO_DATE_TIME)
    )
  }

  lazy val LatestRelease: Option[GatlingVersion] = LatestGatlingRelease.load()
}

final case class GatlingVersion(fullVersion: String, releaseDate: ZonedDateTime) {
  val minorVersion: String = fullVersion.split('.').take(2).mkString(".")
  val majorVersion: String = fullVersion.substring(0, fullVersion.indexOf('.'))
  def isDev: Boolean = fullVersion.endsWith("-SNAPSHOT")
  def isEnterprise: Boolean = fullVersion.contains(".FL")
}

private object LatestGatlingRelease extends StrictLogging {
  private sealed abstract class FetchResult(val lastCheckTimestamp: Long, maxAgeMillis: Long) extends Product with Serializable {
    def valid(nowMillis: Long): Boolean = nowMillis - lastCheckTimestamp < maxAgeMillis
  }

  private object FetchResult {
    final case class Success(override val lastCheckTimestamp: Long, latestRelease: GatlingVersion) extends FetchResult(lastCheckTimestamp, 24 * 3600)
    final case class Failure(override val lastCheckTimestamp: Long) extends FetchResult(lastCheckTimestamp, 3600)
  }

  private val Prefs = Preferences.userRoot().node("/io/gatling/latestVersion")
  private val LastCheckTimestampPref = "lastCheckTimestamp"
  private val LastCheckSuccessPref = "lastCheckSuccess"
  private val LatestReleaseNumberPref = "latestReleaseNumber"
  private val LatestReleaseDatePref = "latestReleaseDate"

  private val MavenCentralQuery = "https://search.maven.org/solrsearch/select?q=g:io.gatling+AND+a:gatling-core+AND+p:jar&rows=1&wt=json&core=gav"
  private val MavenCentralQueryTimeoutMillis = 1000
  private val MavenCentralQueryVersionRegex = """"v":\s*"(.+?)"""".r
  private val MavenCentralQueryTimestampRegex = """"timestamp":\s*(\d+)""".r

  private def loadPersisted(): Try[FetchResult] =
    Try {
      val lastCheckTimestamp = Prefs.getLong(LastCheckTimestampPref, 0)
      if (Prefs.getBoolean(LastCheckSuccessPref, false)) {
        FetchResult.Success(
          lastCheckTimestamp,
          GatlingVersion(
            Option(Prefs.get(LatestReleaseNumberPref, null)).getOrElse(throw new IllegalStateException(s"$LatestReleaseNumberPref is missing")),
            Option(Prefs.get(LatestReleaseDatePref, null))
              .map(ZonedDateTime.parse(_, DateTimeFormatter.ISO_DATE_TIME))
              .getOrElse(throw new IllegalStateException(s"$LatestReleaseDatePref is missing"))
          )
        )
      } else {
        FetchResult.Failure(lastCheckTimestamp)
      }
    }

  private def persist(lastCheck: FetchResult): Try[Unit] =
    Try {
      Prefs.putLong(LastCheckTimestampPref, lastCheck.lastCheckTimestamp)
      lastCheck match {
        case FetchResult.Success(_, latestRelease) =>
          Prefs.putBoolean(LastCheckSuccessPref, true)
          Prefs.put(LatestReleaseNumberPref, latestRelease.fullVersion)
          Prefs.putLong(LastCheckSuccessPref, latestRelease.releaseDate.toInstant.toEpochMilli)

        case _ =>
          Prefs.putBoolean(LastCheckSuccessPref, false)
          Prefs.remove(LatestReleaseNumberPref)
          Prefs.remove(LastCheckSuccessPref)
      }
      Prefs.flush()
    }

  private def fetchLatestReleaseFromMavenCentral(): Try[GatlingVersion] =
    Try {
      val conn = new URI(MavenCentralQuery).toURL.openConnection().asInstanceOf[HttpURLConnection]

      try {
        conn.setReadTimeout(MavenCentralQueryTimeoutMillis)
        conn.setConnectTimeout(MavenCentralQueryTimeoutMillis)
        conn.setDoInput(true)
        conn.setDoOutput(false)
        conn.setUseCaches(true)
        conn.setRequestMethod("GET")
        conn.setRequestProperty("Connection", "close")

        val response = new String(conn.getInputStream.readAllBytes(), StandardCharsets.UTF_8)
        parseMavenCentralResponse(response)
      } finally {
        conn.disconnect()
      }
    }

  private[util] def parseMavenCentralResponse(response: String): GatlingVersion = {
    val version =
      MavenCentralQueryVersionRegex
        .findFirstMatchIn(response)
        .getOrElse(throw new IllegalArgumentException(s"Failed to find version field in $response"))
        .group(1)
    val timestamp =
      MavenCentralQueryTimestampRegex
        .findFirstMatchIn(response)
        .getOrElse(throw new IllegalArgumentException(s"Failed to find timestamp field in $response"))
        .group(1)
        .toLong
    GatlingVersion(version, ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC))
  }

  private implicit final class OnFailureTry[T](val t: Try[T]) extends AnyVal {
    def logDebugOnFailure(message: String): Try[T] =
      t.recoverWith { case NonFatal(e) =>
        logger.debug(message, e)
        t
      }
  }

  def load(): Option[GatlingVersion] = {
    val now = System.currentTimeMillis()
    loadPersisted().logDebugOnFailure("Failed to load persisted latest release") match {
      case Success(lastCheck) if lastCheck.valid(now) =>
        lastCheck match {
          case FetchResult.Success(_, latestRelease) => Some(latestRelease)
          case _                                     => None
        }

      case _ =>
        val maybeFetched = fetchLatestReleaseFromMavenCentral().logDebugOnFailure("Failed to fetch latest release from maven central")
        val fetchResult = maybeFetched.fold(
          _ => FetchResult.Failure(now),
          FetchResult.Success(now, _)
        )
        persist(fetchResult).logDebugOnFailure("Failed to persist last version check")
        maybeFetched.toOption
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy