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

pomutil.POM.scala Maven / Gradle / Ivy

//
// pom-util - a Scala library for reading Maven POM files
// http://github.com/samskivert/pom-util/blob/master/LICENSE

package pomutil

import java.io.File
import java.util.regex.Pattern

import scala.collection.mutable.{ArrayBuffer, Set => MSet}
import scala.xml.{XML, Node}

/**
  * Project metadata.
  */
class POM (
  val parent :Option[POM],
  val parentDep :Option[Dependency],
  val file :Option[File],
  elem :Node
) {
  import POM._

  lazy val modelVersion :String = attr("modelVersion") getOrElse("4.0.0")

  lazy val groupId    :String = iattr("groupId", _.groupId)
  lazy val artifactId :String = attr("artifactId") getOrElse("missing")
  lazy val version    :String = iattr("version", _.version)
  lazy val packaging  :String = iattr("packaging", _.packaging)

  lazy val name        :Option[String] = attr("name")
  lazy val description :Option[String] = attr("description")
  lazy val url         :Option[String] = attr("url")

  lazy val modules    :Seq[String]  = (elem \ "modules" \\ "module") map(_.text.trim)
  lazy val profiles   :Seq[Profile] = (elem \ "profiles" \\ "profile") map(new Profile(this, _))
  lazy val properties :Map[String,String] =
    (elem \ "properties" \ "_") map(n => (n.label.trim, n.text.trim)) toMap

  lazy val depends :Seq[Dependency] = manageDepends(
    (elem \ "dependencies" \ "dependency") map(Dependency.fromXML(subProps)))
  lazy val dependMgmt :Map[String,Dependency] =
    (elem \ "dependencyManagement" \ "dependencies" \ "dependency") map(
      Dependency.fromXML(subProps)) map(d => (d.mgmtKey, d)) toMap

  /** Build properties like `sourceDirectory` and other simple stuff. */
  lazy val buildProps :Map[String,String] =
    (elem \ "build" \ "_") filter(n => knownBuildProps(n.label.trim)) map(
      n => (n.label.trim, n.text.trim)) toMap // TODO: extract deeper stuffs

  /** Returns an identifier that encompases the group, artifact and version. */
  def id = groupId + ":" + artifactId + ":" + version

  /** Returns all modules defined in the main POM and in all profiles. */
  def allModules = modules ++ profiles.flatMap(_.modules)

  // TODO: other bits

  /** Looks up a POM attribute, which may include properties defined in the POM as well as basic
    * project attributes like `project.version`, etc. */
  def getAttr (name :String) :Option[String] =
    // TODO: support env.x and Java system properties?
    getProjectAttr(name) orElse properties.get(name) orElse parent.flatMap(_.getAttr(name))

  /** Returns a dependency on the (optionally classified) artifact described by this POM. */
  def toDependency (classifier :Option[String] = None,
                    scope :String = Dependency.DefaultScope,
                    optional :Boolean = false) =
    Dependency(groupId, artifactId, version, packaging, classifier, scope, optional)

  /** Returns true if this POM declares a snapshot artifact, false otherwise. */
  def isSnapshot = version.endsWith("-SNAPSHOT")

  /** Extracts the text of an attribute from the supplied element and substitutes properties. */
  def attr (elem :Node, name :String) :Option[String] = XMLUtil.text(elem, name) map(subProps)

  /** Computes this POM's transitive dependencies. Exclusions are honored, and conflicts are resolved
    * using the standard "distance from root POM" Maven semantics. Direct dependencies with scopes
    * other than `compile` and `test` are included for this project, but not transitively, also per
    * standard Maven semantics.
    *
    * @param forTest whether to include test dependencies.
    */
  def transitiveDepends (forTest :Boolean) :Seq[Dependency] = {
    val haveDeps = MSet[(String,String)]()
    val allDeps = ArrayBuffer[Dependency]()
    def key (d :Dependency) = (d.groupId, d.artifactId)

    // we expand our dependency tree one "layer" at a time; we start with the depends at distance
    // one from the project (its direct dependencies), then we compute all of the direct
    // dependencies of those depends (distance two) and filter out any that are already satisfied
    // (this enforces the "closest to the root POM" conflict resolution strategy), then we repeat
    // the process, expanding to distance three and so forth, until we discover no new depends

    // TODO: handle exclusions

    def extract (deps :Seq[Dependency], mapper :(Dependency => Dependency)) {
      haveDeps ++= (deps map key)
      allDeps ++= deps
      val newdeps = for { dep <- deps
                          pom <- dep.localPOM.flatMap(fromFile).toSeq
                          dd <- pom.depends filterNot(d => dep.exclusions((d.groupId, d.artifactId)))
                          if (dd.scope == "compile" && !dd.optional && !haveDeps(key(dd)))
                        } yield mapper(dd)
      // we might encounter the same dep from two parents, so we .distinct to consolidate
      if (!newdeps.isEmpty) extract(newdeps.distinct, mapper)
    }

    val (compDeps, testDeps) = depends partition(_.scope != "test")
    extract(compDeps, d => d)
    if (forTest) extract(testDeps, _.copy(scope="test"))

    allDeps.toSeq
  }

  /** A function that substitutes this POM's properties into the supplied text. */
  val subProps = (text :String) => {
    val m = PropRe.matcher(text)
    val sb = new StringBuffer
    while (m.find()) {
      val name = m.group(1)
      m.appendReplacement(sb, getAttr(name).getOrElse("\\$!{" + name + "}"))
    }
    m.appendTail(sb)
    sb.toString
  }

  override def toString = groupId + ":" + artifactId + ":" + version + parent.map(
    " <- " + _).getOrElse("")

  private def iattr (name :String, pfunc :POM => String) =
    attr(name) orElse(parent map pfunc) getOrElse("missing")

  private def getProjectAttr (key :String) :Option[String] =
    if (!key.startsWith("project.")) None else key.substring(8) match {
      case "groupId" => Some(groupId)
      case "artifactId" => Some(artifactId)
      case "version" => Some(version)
      case "packaging" => Some(packaging)
      case "name" => name
      case "description" => description
      case "url" => url
      case pkey if (pkey.startsWith("parent.")) => pkey.substring(7) match {
        case "groupId" => parentDep.map(_.groupId)
        case "artifactId" => parentDep.map(_.artifactId)
        case "version" => parentDep.map(_.version)
        case _ => None
      }
      case _ => None
    }

  // replaces any depends with "canonical" version from depmgmt section
  private def manageDepends (depends :Seq[Dependency]) :Seq[Dependency] = {
    val managed = depends map(d => dependMgmt.get(d.mgmtKey) match {
      case Some(md) => md.copy(scope = d.scope, optional = d.optional)
      case None     => d
    })
    (parent :\ managed)(_ manageDepends _)
  }

  private def attr (name :String) :Option[String] = attr(elem, name)
}

object POM {
  import XMLUtil._

  /** Parses the POM in the specified file. */
  def fromFile (file :File) :Option[POM] = fromXML(XML.loadFile(file), Some(file.getAbsoluteFile))

  /** Parses a POM from the supplied XML. */
  def fromXML (node :Node, file :Option[File]) :Option[POM] = node match {
    case elem if (elem.label == "project") => {
      val parentDep = (elem \ "parent").headOption map(Dependency.fromXML) map(
        _.copy(`type` = "pom"))
      val parentPath = (elem \ "parent" \ "relativePath").headOption map(_.text.trim) getOrElse(
        ".." + File.separator + "pom.xml")
      val parentFile = file map(f => new File(f.getParentFile, parentPath).getCanonicalFile)
      val parent = try {
        localParent(parentFile, parentDep) orElse installedParent(parentDep)
      } catch {
        case e :Throwable =>
          warn("Failed to read parent pom (" + parentDep + "): " + e.getMessage) ; None
      }
      // if we have a parent dep but were unable to find the parent POM, issue a warning
      if (parentDep.isDefined && !parent.isDefined) {
        warn(text(elem, "artifactId").getOrElse("unknown") + " missing parent: " + parentDep.get.id)
      }
      Some(new POM(parent, parentDep, file, elem))
    }
    case _ => None
  }

  private val knownBuildProps = Set("sourceDirectory", "testSourceDirectory")

  private def localParent (file :Option[File], parentDep :Option[Dependency]) = for {
    pdep <- parentDep
    parentFile <- file
    if (parentFile.exists)
    pom <- fromFile(parentFile)
    if (pom.toDependency() == pdep)
  } yield pom

  private def installedParent (parentDep :Option[Dependency]) = for {
    pdep <- parentDep
    pfile <- pdep.localPOM
    pom <- fromFile(pfile)
  } yield pom

  private def warn (msg :String) {
    Console.err.println("!!! " + msg)
  }

  private val PropRe = Pattern.compile("\\$\\{([^}]+)\\}")
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy