diff --git a/core-jvm/src/main/scala/coursier/core/MavenRepository.scala b/core-jvm/src/main/scala/coursier/core/MavenRepository.scala index 5b39acf82..32b182b38 100644 --- a/core-jvm/src/main/scala/coursier/core/MavenRepository.scala +++ b/core-jvm/src/main/scala/coursier/core/MavenRepository.scala @@ -49,7 +49,15 @@ case class MavenRepository( val url = new URL(urlStr) def log = Task(logger.foreach(_.downloading(urlStr))) - def get = MavenRepository.readFully(url.openStream()) + def get = { + val conn = url.openConnection() + // Dummy user-agent instead of the default "Java/...", + // so that we are not returned incomplete/erroneous metadata + // (Maven 2 compatibility? - happens for snapshot versioning metadata, + // this is SO FUCKING CRAZY) + conn.setRequestProperty("User-Agent", "") + MavenRepository.readFully(conn.getInputStream()) + } log.flatMap(_ => get) } diff --git a/core/src/main/scala/coursier/core/Definitions.scala b/core/src/main/scala/coursier/core/Definitions.scala index 504bf09af..130b48092 100644 --- a/core/src/main/scala/coursier/core/Definitions.scala +++ b/core/src/main/scala/coursier/core/Definitions.scala @@ -56,7 +56,8 @@ case class Project( dependencyManagement: Seq[Dependency], properties: Map[String, String], profiles: Seq[Profile], - versions: Option[Versions] + versions: Option[Versions], + snapshotVersioning: Option[SnapshotVersioning] ) { def moduleVersion = (module, version) } @@ -99,6 +100,25 @@ object Versions { ) } +case class SnapshotVersion( + classifier: String, + extension: String, + value: String, + updated: Option[Versions.DateTime] +) + +case class SnapshotVersioning( + module: Module, + version: String, + latest: String, + release: String, + timestamp: String, + buildNumber: Option[Int], + localCopy: Option[Boolean], + lastUpdated: Option[Versions.DateTime], + snapshotVersions: Seq[SnapshotVersion] +) + case class Artifact( url: String, checksumUrls: Map[String, String], diff --git a/core/src/main/scala/coursier/core/Repository.scala b/core/src/main/scala/coursier/core/Repository.scala index eb8ac7350..61455c96d 100644 --- a/core/src/main/scala/coursier/core/Repository.scala +++ b/core/src/main/scala/coursier/core/Repository.scala @@ -101,6 +101,7 @@ object Repository { case class MavenSource(root: String, ivyLike: Boolean) extends Artifact.Source { import Repository._ + import BaseMavenRepository._ def artifacts( dependency: Dependency, @@ -108,7 +109,7 @@ case class MavenSource(root: String, ivyLike: Boolean) extends Artifact.Source { ): Seq[Artifact] = { def ivyLikePath0(subDir: String, baseSuffix: String, ext: String) = - BaseMavenRepository.ivyLikePath( + ivyLikePath( dependency.module.organization, dependency.module.name, project.version, @@ -120,12 +121,20 @@ case class MavenSource(root: String, ivyLike: Boolean) extends Artifact.Source { val path = if (ivyLike) ivyLikePath0(dependency.attributes.`type` + "s", "", dependency.attributes.`type`) - else + else { + val versioning = + project + .snapshotVersioning + .flatMap(versioning => + mavenVersioning(versioning, dependency.attributes.classifier, dependency.attributes.`type`) + ) + dependency.module.organization.split('.').toSeq ++ Seq( dependency.module.name, project.version, - s"${dependency.module.name}-${project.version}${Some(dependency.attributes.classifier).filter(_.nonEmpty).map("-"+_).mkString}.${dependency.attributes.`type`}" + s"${dependency.module.name}-${versioning getOrElse project.version}${Some(dependency.attributes.classifier).filter(_.nonEmpty).map("-"+_).mkString}.${dependency.attributes.`type`}" ) + } var artifact = Artifact( @@ -139,6 +148,9 @@ case class MavenSource(root: String, ivyLike: Boolean) extends Artifact.Source { if (dependency.attributes.`type` == "jar") { artifact = artifact.withDefaultSignature + // FIXME Snapshot versioning of sources and javadoc is not taken into account here. + // Will be ok if it's the same as the main JAR though. + artifact = if (ivyLike) { val srcPath = root + ivyLikePath0("srcs", "-sources", "jar").mkString("/") @@ -181,6 +193,17 @@ object BaseMavenRepository { s"$name$baseSuffix.$ext" ) + def mavenVersioning( + snapshotVersioning: SnapshotVersioning, + classifier: String, + extension: String + ): Option[String] = + snapshotVersioning + .snapshotVersions + .find(v => v.classifier == classifier && v.extension == extension) + .map(_.value) + .filter(_.nonEmpty) + } abstract class BaseMavenRepository( @@ -198,16 +221,27 @@ abstract class BaseMavenRepository( val source = MavenSource(root, ivyLike) - def projectArtifact(module: Module, version: String): Artifact = { + def projectArtifact( + module: Module, + version: String, + versioningValue: Option[String] + ): Artifact = { val path = ( if (ivyLike) - ivyLikePath(module.organization, module.name, version, "poms", "", "pom") + ivyLikePath( + module.organization, + module.name, + versioningValue getOrElse version, + "poms", + "", + "pom" + ) else module.organization.split('.').toSeq ++ Seq( module.name, version, - s"${module.name}-$version.pom" + s"${module.name}-${versioningValue getOrElse version}.pom" ) ) .map(encodeURIComponent) @@ -242,6 +276,32 @@ abstract class BaseMavenRepository( Some(artifact) } + def snapshotVersioningArtifact( + module: Module, + version: String + ): Option[Artifact] = + if (ivyLike) None + else { + val path = ( + module.organization.split('.').toSeq ++ Seq( + module.name, + version, + "maven-metadata.xml" + ) + ) .map(encodeURIComponent) + + val artifact = + Artifact( + path.mkString("/"), + Map.empty, + Map.empty, + Attributes("pom", "") + ) + .withDefaultChecksums + + Some(artifact) + } + def versions( module: Module, cachePolicy: CachePolicy = CachePolicy.Default @@ -265,14 +325,80 @@ abstract class BaseMavenRepository( ) } + def snapshotVersioning( + module: Module, + version: String, + cachePolicy: CachePolicy = CachePolicy.Default + ): EitherT[Task, String, SnapshotVersioning] = { + + EitherT( + snapshotVersioningArtifact(module, version) match { + case None => Task.now(-\/("Not supported")) + case Some(artifact) => + fetch(artifact, cachePolicy) + .run + .map(eitherStr => + for { + str <- eitherStr + xml <- \/.fromEither(compatibility.xmlParse(str)) + _ <- if (xml.label == "metadata") \/-(()) else -\/("Metadata not found") + snapshotVersioning <- Xml.snapshotVersioning(xml) + } yield snapshotVersioning + ) + } + ) + } + def findNoInterval( module: Module, version: String, cachePolicy: CachePolicy + ): EitherT[Task, String, Project] = + EitherT{ + def withSnapshotVersioning = + snapshotVersioning(module, version, cachePolicy) + .flatMap { snapshotVersioning => + val versioningOption = + mavenVersioning(snapshotVersioning, "", "jar") + .orElse(mavenVersioning(snapshotVersioning, "", "")) + + versioningOption match { + case None => + EitherT[Task, String, Project]( + Task.now(-\/("No snapshot versioning value found")) + ) + case versioning @ Some(_) => + findVersioning(module, version, versioning, cachePolicy) + .map(_.copy(snapshotVersioning = Some(snapshotVersioning))) + } + } + + findVersioning(module, version, None, cachePolicy) + .run + .flatMap{ eitherProj => + if (eitherProj.isLeft) + withSnapshotVersioning + .run + .map(eitherProj0 => + if (eitherProj0.isLeft) + eitherProj + else + eitherProj0 + ) + else + Task.now(eitherProj) + } + } + + def findVersioning( + module: Module, + version: String, + versioningValue: Option[String], + cachePolicy: CachePolicy ): EitherT[Task, String, Project] = { EitherT { - fetch(projectArtifact(module, version), cachePolicy) + fetch(projectArtifact(module, version, versioningValue), cachePolicy) .run .map(eitherStr => for { diff --git a/core/src/main/scala/coursier/core/Xml.scala b/core/src/main/scala/coursier/core/Xml.scala index caa2a3156..9bd94700e 100644 --- a/core/src/main/scala/coursier/core/Xml.scala +++ b/core/src/main/scala/coursier/core/Xml.scala @@ -210,10 +210,24 @@ object Xml { depMgmts, properties.toMap, profiles, + None, None ) } + def parseDateTime(s: String): Option[Versions.DateTime] = + if (s.length == 14 && s.forall(_.isDigit)) + Some(Versions.DateTime( + s.substring(0, 4).toInt, + s.substring(4, 6).toInt, + s.substring(6, 8).toInt, + s.substring(8, 10).toInt, + s.substring(10, 12).toInt, + s.substring(12, 14).toInt + )) + else + None + def versions(node: Node): String \/ Versions = { import Scalaz._ @@ -230,24 +244,111 @@ object Xml { release = text(xmlVersioning, "release", "Release version") .getOrElse("") - versions <- xmlVersioning.child + versionsOpt = xmlVersioning.child .find(_.label == "versions") .map(_.child.filter(_.label == "version").flatMap(_.child.collectFirst{case Text(t) => t})) - .toRightDisjunction("Version list not found in metadata") lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time") .toOption - .filter(s => s.length == 14 && s.forall(_.isDigit)) - .map(s => Versions.DateTime( - s.substring(0, 4).toInt, - s.substring(4, 6).toInt, - s.substring(6, 8).toInt, - s.substring(8, 10).toInt, - s.substring(10, 12).toInt, - s.substring(12, 14).toInt - )) + .flatMap(parseDateTime) - } yield Versions(latest, release, versions.toList, lastUpdatedOpt) + } yield Versions(latest, release, versionsOpt.map(_.toList).getOrElse(Nil), lastUpdatedOpt) + } + + def snapshotVersion(node: Node): String \/ SnapshotVersion = { + def textOrEmpty(name: String, desc: String) = + text(node, name, desc) + .toOption + .getOrElse("") + + val classifier = textOrEmpty("classifier", "Classifier") + val ext = textOrEmpty("extension", "Extensions") + val value = textOrEmpty("value", "Value") + + val updatedOpt = text(node, "updated", "Updated") + .toOption + .flatMap(parseDateTime) + + \/-(SnapshotVersion( + classifier, + ext, + value, + updatedOpt + )) + } + + def snapshotVersioning(node: Node): String \/ SnapshotVersioning = { + import Scalaz._ + + // FIXME Quite similar to Versions above + for { + organization <- text(node, "groupId", "Organization") + name <- text(node, "artifactId", "Name") + version = readVersion(node) + + xmlVersioning <- node.child + .find(_.label == "versioning") + .toRightDisjunction("Versioning info not found in metadata") + + latest = text(xmlVersioning, "latest", "Latest version") + .getOrElse("") + release = text(xmlVersioning, "release", "Release version") + .getOrElse("") + + versionsOpt = xmlVersioning.child + .find(_.label == "versions") + .map(_.child.filter(_.label == "version").flatMap(_.child.collectFirst{case Text(t) => t})) + + lastUpdatedOpt = text(xmlVersioning, "lastUpdated", "Last update date and time") + .toOption + .flatMap(parseDateTime) + + xmlSnapshotOpt = xmlVersioning.child + .find(_.label == "snapshot") + + timestamp = xmlSnapshotOpt + .flatMap( + text(_, "timestamp", "Snapshot timestamp") + .toOption + ) + .getOrElse("") + + buildNumber = xmlSnapshotOpt + .flatMap( + text(_, "buildNumber", "Snapshot build number") + .toOption + ) + .filter(s => s.nonEmpty && s.forall(_.isDigit)) + .map(_.toInt) + + localCopy = xmlSnapshotOpt + .flatMap( + text(_, "localCopy", "Snapshot local copy") + .toOption + ) + .collect{ + case "true" => true + case "false" => false + } + + xmlSnapshotVersions = xmlVersioning.child + .find(_.label == "snapshotVersions") + .map(_.child.filter(_.label == "snapshotVersion")) + .getOrElse(Seq.empty) + snapshotVersions <- xmlSnapshotVersions + .toList + .traverseU(snapshotVersion) + } yield SnapshotVersioning( + Module(organization, name), + version, + latest, + release, + timestamp, + buildNumber, + localCopy, + lastUpdatedOpt, + snapshotVersions + ) } } diff --git a/core/src/test/scala/coursier/test/package.scala b/core/src/test/scala/coursier/test/package.scala index b2feafef9..ebcf0b6ab 100644 --- a/core/src/test/scala/coursier/test/package.scala +++ b/core/src/test/scala/coursier/test/package.scala @@ -30,7 +30,8 @@ package object test { dependencyManagement: Seq[Dependency] = Seq.empty, properties: Map[String, String] = Map.empty, profiles: Seq[Profile] = Seq.empty, - versions: Option[core.Versions] = None): Project = - core.Project(module, version, dependencies, parent, dependencyManagement, properties, profiles, versions) + versions: Option[core.Versions] = None, + snapshotVersioning: Option[core.SnapshotVersioning] = None): Project = + core.Project(module, version, dependencies, parent, dependencyManagement, properties, profiles, versions, snapshotVersioning) } }