From 4d76f2341f7f55d2c214a49bba200a5843c56850 Mon Sep 17 00:00:00 2001 From: Matthew de Detrich Date: Tue, 18 Apr 2023 13:48:59 +0200 Subject: [PATCH] Add support for additional columns --- .../sbtlicensereport/SbtLicenseReport.scala | 12 +++- .../sbtlicensereport/license/Column.scala | 48 +++++++++++++ .../license/LicenseReport.scala | 47 ++++++++----- .../license/LicenseReportConfiguration.scala | 3 +- .../license/TargetLanguage.scala | 70 +++++++++++-------- .../custom-report/example.sbt | 4 +- 6 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 src/main/scala/sbtlicensereport/license/Column.scala diff --git a/src/main/scala/sbtlicensereport/SbtLicenseReport.scala b/src/main/scala/sbtlicensereport/SbtLicenseReport.scala index 6299b11..6c17f48 100644 --- a/src/main/scala/sbtlicensereport/SbtLicenseReport.scala +++ b/src/main/scala/sbtlicensereport/SbtLicenseReport.scala @@ -16,7 +16,9 @@ object SbtLicenseReport extends AutoPlugin { type TargetLanguage = sbtlicensereport.license.TargetLanguage type LicenseReportConfiguration = sbtlicensereport.license.LicenseReportConfiguration type DepModuleInfo = sbtlicensereport.license.DepModuleInfo + type Column = sbtlicensereport.license.Column val DepModuleInfo = sbtlicensereport.license.DepModuleInfo + val Column = sbtlicensereport.license.Column def LicenseReportConfiguration = sbtlicensereport.license.LicenseReportConfiguration def Html = sbtlicensereport.license.Html def MarkDown = sbtlicensereport.license.MarkDown @@ -34,6 +36,8 @@ object SbtLicenseReport extends AutoPlugin { val dumpLicenseReportAnyProject = taskKey[File]( "Dumps a report file against all projects of the license report (using the target language) and combines it into a single file." ) + val licenseReportColumns = + settingKey[Seq[Column]]("Additional columns to be added to the final report") val licenseReportDir = settingKey[File]("The location where we'll write the license reports.") val licenseReportStyleRules = settingKey[Option[String]]("The style rules for license report styling.") val licenseReportTitle = settingKey[String]("The name of the license report.") @@ -75,12 +79,14 @@ object SbtLicenseReport extends AutoPlugin { val ignore = update.value val overrides = licenseOverrides.value.lift val depExclusions = licenseDepExclusions.value.lift + val originatingModule = DepModuleInfo(organization.value, name.value, version.value) license.LicenseReport.makeReport( ivyModule.value, licenseConfigurations.value, licenseSelection.value, overrides, depExclusions, + originatingModule, streams.value.log ) }, @@ -101,7 +107,8 @@ object SbtLicenseReport extends AutoPlugin { notesLookup, licenseFilter.value, dir, - styleRules + styleRules, + licenseReportColumns.value ) Seq(config) }, @@ -137,6 +144,7 @@ object SbtLicenseReport extends AutoPlugin { licenseDepExclusions := PartialFunction.empty, licenseFilter := TypeFunctions.const(true), licenseReportStyleRules := None, - licenseReportTypes := Seq(MarkDown, Html, Csv) + licenseReportTypes := Seq(MarkDown, Html, Csv), + licenseReportColumns := Seq(Column.Category, Column.License, Column.Dependency) ) } diff --git a/src/main/scala/sbtlicensereport/license/Column.scala b/src/main/scala/sbtlicensereport/license/Column.scala new file mode 100644 index 0000000..3f0fa47 --- /dev/null +++ b/src/main/scala/sbtlicensereport/license/Column.scala @@ -0,0 +1,48 @@ +package sbtlicensereport.license + +trait Column { + def columnName: String + + def render(depLicense: DepLicense, language: TargetLanguage): String +} + +object Column { + case object Category extends Column { + override val columnName: String = "Category" + + override def render(depLicense: DepLicense, language: TargetLanguage): String = + depLicense.license.category.name + } + + case object License extends Column { + override val columnName: String = "License" + + override def render(depLicense: DepLicense, language: TargetLanguage): String = + language.createHyperLink(depLicense.license.url, depLicense.license.name) + } + + case object Dependency extends Column { + override val columnName: String = "Dependency" + + override def render(depLicense: DepLicense, language: TargetLanguage): String = + depLicense.homepage match { + case None => depLicense.module.toString + case Some(url) => language.createHyperLink(url.toExternalForm, depLicense.module.toString) + } + } + + case object Configuration extends Column { + override val columnName: String = "Maven/Ivy Configurations" + + override def render(depLicense: DepLicense, language: TargetLanguage): String = { + depLicense.configs.mkString(",") + } + } + + case object OriginatingArtifactName extends Column { + override val columnName: String = "Originating Artifact" + + override def render(depLicense: DepLicense, language: TargetLanguage): String = + depLicense.originatingModule.name + } +} diff --git a/src/main/scala/sbtlicensereport/license/LicenseReport.scala b/src/main/scala/sbtlicensereport/license/LicenseReport.scala index 44c7101..352bc56 100644 --- a/src/main/scala/sbtlicensereport/license/LicenseReport.scala +++ b/src/main/scala/sbtlicensereport/license/LicenseReport.scala @@ -10,7 +10,13 @@ import sbtlicensereport.SbtCompat._ case class DepModuleInfo(organization: String, name: String, version: String) { override def toString = s"${organization} # ${name} # ${version}" } -case class DepLicense(module: DepModuleInfo, license: LicenseInfo, homepage: Option[URL], configs: Set[String]) { +case class DepLicense( + module: DepModuleInfo, + license: LicenseInfo, + homepage: Option[URL], + configs: Set[String], + originatingModule: DepModuleInfo +) { override def toString = s"$module ${homepage.map(url => s" from $url")} on $license in ${configs.mkString("(", ",", ")")}" } @@ -54,18 +60,17 @@ object LicenseReport { withPrintableFile(reportFile) { print => print(language.documentStart(title, reportStyleRules)) print(makeHeader(language)) - print(language.tableHeader("Category", "License", "Dependency", "Notes")) + print(language.tableHeader("Notes", config.licenseReportColumns.map(_.columnName): _*)) val rendered = (ordered map { dep => - val licenseLink = language.createHyperLink(dep.license.url, dep.license.name) - val moduleLink = dep.homepage match { - case None => dep.module.toString - case Some(url) => language.createHyperLink(url.toExternalForm, dep.module.toString) - } - (dep.license.category.name, licenseLink, moduleLink, notes(dep.module) getOrElse "") + val notesRendered = notes(dep.module) getOrElse "" + ( + notesRendered, + config.licenseReportColumns map (_.render(dep, language)) + ) }).distinct - for ((name, licenseLink, moduleLink, notes) <- rendered) { - print(language.tableRow(name, licenseLink, moduleLink, notes)) + for ((notes, rest) <- rendered) { + print(language.tableRow(notes, rest: _*)) } print(language.tableEnd) print(language.documentEnd) @@ -84,11 +89,12 @@ object LicenseReport { licenseSelection: Seq[LicenseCategory], overrides: DepModuleInfo => Option[LicenseInfo], exclusions: DepModuleInfo => Option[Boolean], + originatingModule: DepModuleInfo, log: Logger ): LicenseReport = { val (report, err) = resolve(module, log) err foreach (x => throw x) // Bail on error - makeReportImpl(report, configs, licenseSelection, overrides, exclusions, log) + makeReportImpl(report, configs, licenseSelection, overrides, exclusions, originatingModule, log) } /** @@ -118,7 +124,8 @@ object LicenseReport { private def pickLicenseForDep( dep: IvyNode, configs: Set[String], - categories: Seq[LicenseCategory] + categories: Seq[LicenseCategory], + originatingModule: DepModuleInfo ): Option[DepLicense] = for { d <- Option(dep) @@ -138,17 +145,24 @@ object LicenseReport { .apply(Some(url(loc))) ) // TODO - grab configurations. - } yield DepLicense(getModuleInfo(dep), pickLicense(categories)(licenses), homepage, filteredConfigs) + } yield DepLicense( + getModuleInfo(dep), + pickLicense(categories)(licenses), + homepage, + filteredConfigs, + originatingModule + ) private def getLicenses( report: ResolveReport, configs: Set[String] = Set.empty, - categories: Seq[LicenseCategory] = LicenseCategory.all + categories: Seq[LicenseCategory] = LicenseCategory.all, + originatingModule: DepModuleInfo ): Seq[DepLicense] = { import collection.JavaConverters._ for { dep <- report.getDependencies.asInstanceOf[java.util.List[IvyNode]].asScala - report <- pickLicenseForDep(dep, configs, categories) + report <- pickLicenseForDep(dep, configs, categories, originatingModule) } yield report } @@ -158,9 +172,10 @@ object LicenseReport { categories: Seq[LicenseCategory], overrides: DepModuleInfo => Option[LicenseInfo], exclusions: DepModuleInfo => Option[Boolean], + originatingModule: DepModuleInfo, log: Logger ): LicenseReport = { - val licenses = getLicenses(report, configs, categories) filterNot { dep => + val licenses = getLicenses(report, configs, categories, originatingModule) filterNot { dep => exclusions(dep.module).getOrElse(false) } map { l => overrides(l.module) match { diff --git a/src/main/scala/sbtlicensereport/license/LicenseReportConfiguration.scala b/src/main/scala/sbtlicensereport/license/LicenseReportConfiguration.scala index ed0603a..f82b48c 100644 --- a/src/main/scala/sbtlicensereport/license/LicenseReportConfiguration.scala +++ b/src/main/scala/sbtlicensereport/license/LicenseReportConfiguration.scala @@ -10,5 +10,6 @@ case class LicenseReportConfiguration( notes: DepModuleInfo => Option[String], licenseFilter: LicenseCategory => Boolean, reportDir: File, - reportStyleRules: Option[String] = None + reportStyleRules: Option[String] = None, + licenseReportColumns: Seq[Column] ) diff --git a/src/main/scala/sbtlicensereport/license/TargetLanguage.scala b/src/main/scala/sbtlicensereport/license/TargetLanguage.scala index 0e9fb39..72fa844 100644 --- a/src/main/scala/sbtlicensereport/license/TargetLanguage.scala +++ b/src/main/scala/sbtlicensereport/license/TargetLanguage.scala @@ -18,10 +18,10 @@ sealed trait TargetLanguage { def header1(msg: String): String /** The syntax for the header of a table. */ - def tableHeader(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String + def tableHeader(notes: String, columns: String*): String /** The syntax for a row of a table. */ - def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String + def tableRow(notes: String, columns: String*): String /** And a "table" */ def tableEnd: String @@ -38,13 +38,17 @@ case object MarkDown extends TargetLanguage { s"[$content]($link)" def blankLine(): String = "\n" def header1(msg: String): String = s"# $msg\n" - def tableHeader(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = - s""" -$firstColumn | $secondColumn | $thirdColumn | $fourthColumn ---- | --- | --- | --- -""" - def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = - s"$firstColumn | $secondColumn | $thirdColumn | ${escapeHtml(fourthColumn)}\n" + def tableHeader(notes: String, columns: String*): String = { + val all = columns :+ notes + val firstRow = "\n" + all.mkString(" | ") + val secondRow = List.fill(all.size - 1)("").mkString("--- |", " --- |", " ---") + firstRow ++ "\n" ++ secondRow + "\n" + } + def tableRow(notes: String, columns: String*): String = { + val main = columns.mkString("", " | ", " | ") + val notesEscaped = s"${escapeHtml(notes)}\n" + main ++ notesEscaped + } def tableEnd: String = "\n" def markdownEncode(s: String): String = s.flatMap { @@ -71,14 +75,21 @@ case object Html extends TargetLanguage { s"""$content""" def blankLine(): String = "

 

" def header1(msg: String): String = s"

$msg

" - def tableHeader(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = + def tableHeader(notes: String, columns: String*): String = { + val all = columns :+ notes + val th = all.mkString("", "", "") s""" - + $th""" - def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = - s"""""" + } + def tableRow(notes: String, columns: String*): String = { + val main = columns.mkString("""" + + main + notesEscaped + } def tableEnd: String = "
$firstColumn$secondColumn$thirdColumn$fourthColumn
${firstColumn} ${secondColumn} ${thirdColumn} ${htmlEncode( - fourthColumn - )}
""", """ """, """ """) + val notesEscaped = s"${htmlEncode( + notes + )}
" def htmlEncode(s: String) = org.apache.commons.lang3.StringEscapeUtils.escapeHtml4(s) @@ -93,10 +104,15 @@ case object Csv extends TargetLanguage { } def blankLine(): String = "" def header1(msg: String): String = "" - def tableHeader(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = - tableRow(firstColumn, secondColumn, thirdColumn, fourthColumn) - def tableRow(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = - s"""${csvEncode(firstColumn)},${csvEncode(secondColumn)},${csvEncode(thirdColumn)},${csvEncode(fourthColumn)}\n""" + def tableHeader(notes: String, columns: String*): String = { + tableRow(notes, columns: _*) + } + def tableRow(notes: String, columns: String*): String = { + val all = columns :+ notes + val escaped = all map csvEncode + escaped.mkString("", ",", "\n") + } + def tableEnd: String = "" def csvEncode(s: String): String = org.apache.commons.lang3.StringEscapeUtils.escapeCsv(s) } @@ -108,16 +124,14 @@ case object ConfluenceWikiMarkup extends TargetLanguage { def createHyperLink(link: String, content: String): String = s"[${trim(content)}|${trim(link)}]" def blankLine(): String = "\n" def header1(msg: String): String = s"h1.$msg\n" - def tableHeader(firstColumn: String, secondColumn: String, thirdColumn: String, fourthColumn: String): String = { - s"|| $firstColumn || $secondColumn || $thirdColumn || $fourthColumn ||\n" + def tableHeader(notes: String, columns: String*): String = { + val all = columns :+ notes + all.mkString("|| ", " || ", " ||\n") + } + def tableRow(notes: String, columns: String*): String = { + val all = columns :+ notes + all.mkString("| ", " | ", " |\n") } - - def tableRow( - firstColumn: String, - secondColumn: String, - thirdColumn: String, - fourthColumn: String - ): String = s"| $firstColumn | $secondColumn | $thirdColumn | $fourthColumn |\n" def tableEnd: String = "\n" def markdownEncode(s: String): String = s.flatMap { diff --git a/src/sbt-test/dumpLicenseReport/custom-report/example.sbt b/src/sbt-test/dumpLicenseReport/custom-report/example.sbt index 32c95da..57817c7 100644 --- a/src/sbt-test/dumpLicenseReport/custom-report/example.sbt +++ b/src/sbt-test/dumpLicenseReport/custom-report/example.sbt @@ -20,7 +20,9 @@ licenseReportConfigurations += language => language.header1("Testing the configuration"), dep => Option("Default notes"), category => category == LicenseCategory.BSD, - licenseReportDir.value + licenseReportDir.value, + None, + Seq(Column.Category, Column.License, Column.Dependency) ) val check = taskKey[Unit]("check the license report.")