diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala index fb14f9738..5fc7e077d 100644 --- a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala +++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala @@ -2,83 +2,12 @@ package coursier.ivy import coursier.Fetch import coursier.core._ -import scala.annotation.tailrec -import scala.util.matching.Regex + import scalaz._ -import java.util.regex.Pattern.quote - -object IvyRepository { - - val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r - val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r - val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r - - sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable { - require(effectiveStart <= effectiveEnd) - def start = effectiveStart - def end = effectiveEnd - - // FIXME Some kind of validation should be used here, to report all the missing variables, - // not only the first one missing. - def apply(content: String): Map[String, String] => String \/ String - } - object PatternPart { - case class Literal(override val effectiveStart: Int, override val effectiveEnd: Int) extends PatternPart(effectiveStart, effectiveEnd) { - def apply(content: String): Map[String, String] => String \/ String = { - assert(content.length == effectiveEnd - effectiveStart) - val matches = variableRegex.findAllMatchIn(content).toList - - variables => - @tailrec - def helper(idx: Int, matches: List[Regex.Match], b: StringBuilder): String \/ String = - if (idx >= content.length) - \/-(b.result()) - else { - assert(matches.headOption.forall(_.start >= idx)) - matches.headOption.filter(_.start == idx) match { - case Some(m) => - val variableName = content.substring(m.start + 1, m.end - 1) - variables.get(variableName) match { - case None => -\/(s"Variable not found: $variableName") - case Some(value) => - b ++= value - helper(m.end, matches.tail, b) - } - case None => - val nextIdx = matches.headOption.fold(content.length)(_.start) - b ++= content.substring(idx, nextIdx) - helper(nextIdx, matches, b) - } - } - - helper(0, matches, new StringBuilder) - } - } - case class Optional(start0: Int, end0: Int) extends PatternPart(start0 + 1, end0 - 1) { - override def start = start0 - override def end = end0 - - def apply(content: String): Map[String, String] => String \/ String = { - assert(content.length == effectiveEnd - effectiveStart) - val inner = Literal(effectiveStart, effectiveEnd).apply(content) - - variables => - \/-(inner(variables).fold(_ => "", x => x)) - } - } - } - - def substituteProperties(s: String, properties: Map[String, String]): String = - propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) => - val key = s0.substring(m.start + "${".length, m.end - "}".length) - val value = properties.getOrElse(key, "") - s0.take(m.start) + value + s0.drop(m.end) - } - -} case class IvyRepository( pattern: String, + metadataPatternOpt: Option[String] = None, changing: Option[Boolean] = None, properties: Map[String, String] = Map.empty, withChecksums: Boolean = true, @@ -86,62 +15,12 @@ case class IvyRepository( withArtifacts: Boolean = true ) extends Repository { + def metadataPattern: String = metadataPatternOpt.getOrElse(pattern) + import Repository._ - import IvyRepository._ - private val pattern0 = substituteProperties(pattern, properties) - - val parts = { - val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m => - PatternPart.Optional(m.start, m.end) - } - - val len = pattern0.length - - @tailrec - def helper( - idx: Int, - opt: List[PatternPart.Optional], - acc: List[PatternPart] - ): Vector[PatternPart] = - if (idx >= len) - acc.toVector.reverse - else - opt match { - case Nil => - helper(len, Nil, PatternPart.Literal(idx, len) :: acc) - case (opt0 @ PatternPart.Optional(start0, end0)) :: rem => - if (idx < start0) - helper(start0, opt, PatternPart.Literal(idx, start0) :: acc) - else { - assert(idx == start0, s"idx: $idx, start0: $start0") - helper(end0, rem, opt0 :: acc) - } - } - - helper(0, optionalParts, Nil) - } - - assert(pattern0.isEmpty == parts.isEmpty) - if (pattern0.nonEmpty) { - for ((a, b) <- parts.zip(parts.tail)) - assert(a.end == b.start) - assert(parts.head.start == 0) - assert(parts.last.end == pattern0.length) - } - - private val substituteHelpers = parts.map { part => - part(pattern0.substring(part.effectiveStart, part.effectiveEnd)) - } - - def substitute(variables: Map[String, String]): String \/ String = - substituteHelpers.foldLeft[String \/ String](\/-("")) { - case (acc0, helper) => - for { - acc <- acc0 - s <- helper(variables) - } yield acc + s - } + private val pattern0 = Pattern(pattern, properties) + private val metadataPattern0 = Pattern(metadataPattern, properties) // See http://ant.apache.org/ivy/history/latest-milestone/concept.html for a // list of variables that should be supported. @@ -194,7 +73,7 @@ case class IvyRepository( } val retainedWithUrl = retained.flatMap { p => - substitute(variables( + pattern0.substitute(variables( dependency.module, dependency.version, p.`type`, @@ -236,7 +115,7 @@ case class IvyRepository( val eitherArtifact: String \/ Artifact = for { - url <- substitute( + url <- metadataPattern0.substitute( variables(module, version, "ivy", "ivy", "xml", None) ) } yield { @@ -269,4 +148,4 @@ case class IvyRepository( } yield (source, proj) } -} \ No newline at end of file +} diff --git a/core/shared/src/main/scala/coursier/ivy/Pattern.scala b/core/shared/src/main/scala/coursier/ivy/Pattern.scala new file mode 100644 index 000000000..5054bd6dc --- /dev/null +++ b/core/shared/src/main/scala/coursier/ivy/Pattern.scala @@ -0,0 +1,142 @@ +package coursier.ivy + +import scala.annotation.tailrec + +import scalaz._ + +import scala.util.matching.Regex +import java.util.regex.Pattern.quote + +object Pattern { + + val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r + val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r + val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r + + sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable { + require(effectiveStart <= effectiveEnd) + def start = effectiveStart + def end = effectiveEnd + + // FIXME Some kind of validation should be used here, to report all the missing variables, + // not only the first one missing. + def apply(content: String): Map[String, String] => String \/ String + } + object PatternPart { + case class Literal(override val effectiveStart: Int, override val effectiveEnd: Int) extends PatternPart(effectiveStart, effectiveEnd) { + def apply(content: String): Map[String, String] => String \/ String = { + assert(content.length == effectiveEnd - effectiveStart) + val matches = variableRegex.findAllMatchIn(content).toList + + variables => + @tailrec + def helper(idx: Int, matches: List[Regex.Match], b: StringBuilder): String \/ String = + if (idx >= content.length) + \/-(b.result()) + else { + assert(matches.headOption.forall(_.start >= idx)) + matches.headOption.filter(_.start == idx) match { + case Some(m) => + val variableName = content.substring(m.start + 1, m.end - 1) + variables.get(variableName) match { + case None => -\/(s"Variable not found: $variableName") + case Some(value) => + b ++= value + helper(m.end, matches.tail, b) + } + case None => + val nextIdx = matches.headOption.fold(content.length)(_.start) + b ++= content.substring(idx, nextIdx) + helper(nextIdx, matches, b) + } + } + + helper(0, matches, new StringBuilder) + } + } + case class Optional(start0: Int, end0: Int) extends PatternPart(start0 + 1, end0 - 1) { + override def start = start0 + override def end = end0 + + def apply(content: String): Map[String, String] => String \/ String = { + assert(content.length == effectiveEnd - effectiveStart) + val inner = Literal(effectiveStart, effectiveEnd).apply(content) + + variables => + \/-(inner(variables).fold(_ => "", x => x)) + } + } + } + + + def substituteProperties(s: String, properties: Map[String, String]): String = + propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) => + val key = s0.substring(m.start + "${".length, m.end - "}".length) + val value = properties.getOrElse(key, "") + s0.take(m.start) + value + s0.drop(m.end) + } + +} + +case class Pattern( + pattern: String, + properties: Map[String, String] +) { + + import Pattern._ + + private val pattern0 = substituteProperties(pattern, properties) + + val parts = { + val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m => + PatternPart.Optional(m.start, m.end) + } + + val len = pattern0.length + + @tailrec + def helper( + idx: Int, + opt: List[PatternPart.Optional], + acc: List[PatternPart] + ): Vector[PatternPart] = + if (idx >= len) + acc.toVector.reverse + else + opt match { + case Nil => + helper(len, Nil, PatternPart.Literal(idx, len) :: acc) + case (opt0 @ PatternPart.Optional(start0, end0)) :: rem => + if (idx < start0) + helper(start0, opt, PatternPart.Literal(idx, start0) :: acc) + else { + assert(idx == start0, s"idx: $idx, start0: $start0") + helper(end0, rem, opt0 :: acc) + } + } + + helper(0, optionalParts, Nil) + } + + assert(pattern0.isEmpty == parts.isEmpty) + if (pattern0.nonEmpty) { + for ((a, b) <- parts.zip(parts.tail)) + assert(a.end == b.start) + assert(parts.head.start == 0) + assert(parts.last.end == pattern0.length) + } + + private val substituteHelpers = parts.map { part => + part(pattern0.substring(part.effectiveStart, part.effectiveEnd)) + } + + def substitute(variables: Map[String, String]): String \/ String = + substituteHelpers.foldLeft[String \/ String](\/-("")) { + case (acc0, helper) => + for { + acc <- acc0 + s <- helper(variables) + } yield acc + s + } + +} diff --git a/plugin/src/main/scala-2.10/coursier/FromSbt.scala b/plugin/src/main/scala-2.10/coursier/FromSbt.scala index 4e19c3a53..65a139b9c 100644 --- a/plugin/src/main/scala-2.10/coursier/FromSbt.scala +++ b/plugin/src/main/scala-2.10/coursier/FromSbt.scala @@ -115,20 +115,22 @@ object FromSbt { case sbt.FileRepository(_, _, patterns) if patterns.ivyPatterns.lengthCompare(1) == 0 && - patterns.ivyPatterns == patterns.artifactPatterns => + patterns.artifactPatterns.lengthCompare(1) == 0 => Some(IvyRepository( - "file://" + patterns.ivyPatterns.head, + "file://" + patterns.artifactPatterns.head, + metadataPatternOpt = Some("file://" + patterns.ivyPatterns.head), changing = Some(true), properties = ivyProperties )) case sbt.URLRepository(_, patterns) if patterns.ivyPatterns.lengthCompare(1) == 0 && - patterns.ivyPatterns == patterns.artifactPatterns => + patterns.artifactPatterns.lengthCompare(1) == 0 => Some(IvyRepository( - patterns.ivyPatterns.head, + patterns.artifactPatterns.head, + metadataPatternOpt = Some(patterns.ivyPatterns.head), changing = None, properties = ivyProperties ))