Skip to content

Commit

Permalink
fix third party license generation
Browse files Browse the repository at this point in the history
  • Loading branch information
DC2-DanielKrueger committed Oct 8, 2024
1 parent e1e6799 commit 68dd16d
Show file tree
Hide file tree
Showing 12 changed files with 467 additions and 88 deletions.
6 changes: 6 additions & 0 deletions edge-plugins/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ gradlePlugin {
implementationClass = "$group.versionupdater.VersionUpdaterPlugin"
}
}
plugins {
create("third-party-license-generator") {
id = "$group.$name"
implementationClass = "$group.licensethirdparty.ThirdPartyLicenseGeneratorPlugin"
}
}
}

tasks.withType<AbstractArchiveTask>().configureEach {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.hivemq.licensethirdparty

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty

class DependencyReport {

class Root : TypeReference<List<Dependency>>()

class Dependency(

@param:JacksonXmlProperty(localName = "name", isAttribute = true)
val name: String,

@param:JacksonXmlProperty(localName = "file")
val file: String,

@param:JacksonXmlElementWrapper(useWrapping = false)
@param:JacksonXmlProperty(localName = "license")
val licenses: List<License>,
)

class License(

@param:JacksonXmlProperty(localName = "name", isAttribute = true)
val name: String,

@param:JacksonXmlProperty(localName = "url", isAttribute = true)
val url: String?,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.hivemq.licensethirdparty

interface License {
val fullName: String
val url: String?
}

enum class KnownLicense(val id: String, override val fullName: String, override val url: String) : License {
APACHE_2_0("Apache-2.0", "Apache License 2.0", "https://spdx.org/licenses/Apache-2.0.html"),
BLUE_OAK_1_0_0("BlueOak-1.0.0", "Blue Oak Model License 1.0.0", "https://spdx.org/licenses/BlueOak-1.0.0.html"),
BOUNCY_CASTLE("MIT", "Bouncy Castle Licence", "https://www.bouncycastle.org/licence.html"),
BSD_2_CLAUSE("BSD-2-Clause", "BSD 2-Clause \"Simplified\" License", "https://spdx.org/licenses/BSD-2-Clause.html"),
BSD_3_CLAUSE("BSD-3-Clause", "BSD 3-Clause \"New\" or \"Revised\" License", "https://spdx.org/licenses/BSD-3-Clause.html"),
CC_BY_4_0("CC-BY-4.0", "Creative Commons Attribution 4.0 International", "https://spdx.org/licenses/CC-BY-4.0.html"),
CC0_1_0("CC0-1.0", "Creative Commons Zero v1.0 Universal", "https://spdx.org/licenses/CC0-1.0.html"),
CDDL_1_0("CDDL-1.0", "Common Development and Distribution License 1.0", "https://spdx.org/licenses/CDDL-1.0.html"),
CDDL_1_1("CDDL-1.1", "Common Development and Distribution License 1.1", "https://spdx.org/licenses/CDDL-1.1.html"),
// EDL has BSD-3-Clause as SPDX id, documented in the following links:
// https://spdx.org/licenses/BSD-3-Clause.html
// https://www.eclipse.org/org/documents/edl-v10.php
// https://lists.spdx.org/g/Spdx-legal/topic/request_for_adding_eclipse/67981884
EDL_1_0("BSD-3-Clause", "Eclipse Distribution License - v 1.0", "https://www.eclipse.org/org/documents/edl-v10.php"),
EPL_1_0("EPL-1.0", "Eclipse Public License 1.0", "https://spdx.org/licenses/EPL-1.0.html"),
EPL_2_0("EPL-2.0", "Eclipse Public License 2.0", "https://spdx.org/licenses/EPL-2.0.html"),
GO("BSD-3-Clause", "Go License", "https://golang.org/LICENSE"),
ISC("ISC", "ISC License", "https://spdx.org/licenses/ISC.html"),
LGPL_2_1_OR_LATER("LGPL-2.1-or-later", "GNU Lesser General Public License v2.1 or later", "https://spdx.org/licenses/LGPL-2.1-or-later.html"),
MIT("MIT", "MIT License", "https://spdx.org/licenses/MIT.html"),
MIT_0("MIT-0", "MIT No Attribution", "https://spdx.org/licenses/MIT-0.html"),
OFL_1_1("OFL-1.1", "SIL Open Font License 1.1", "https://spdx.org/licenses/OFL-1.1.html"),
PUBLIC_DOMAIN("Public Domain", "Public Domain", ""),
UNICODE_DFS_2016("Unicode-DFS-2016", "Unicode License Agreement - Data Files and Software (2016)", "https://spdx.org/licenses/Unicode-DFS-2016.html"),
UNLICENSE("Unlicense", "Unlicense Yourself: Set Your Code Free", "https://unlicense.org/"),
W3C_19980720("W3C-19980720", "W3C Software Notice and License (1998-07-20)", "https://spdx.org/licenses/W3C-19980720.html"),
ZERO_BSD("0BSD", "BSD Zero Clause License", "https://spdx.org/licenses/0BSD.html"),
}


data class UnknownLicense(override val fullName: String, override val url: String?) : License
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hivemq.licensethirdparty

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.register

class ThirdPartyLicenseGeneratorPlugin : Plugin<Project> {

override fun apply(project: Project) {
project.tasks.register<UpdateThirdPartyLicensesTask>("updateThirdPartyLicenses")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package com.hivemq.licensethirdparty

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
import java.util.*

/**
* Reads the `dependency-license.xml` file created by the `downloadLicenses` task and creates `licenses` and
* `licenses.html` files in the configured [outputDirectory].
*/
abstract class UpdateThirdPartyLicensesTask : DefaultTask() {

companion object {

// defines the artifacts that should be ignored in the third-party license report
private fun shouldIgnore(coordinates: Coordinates) =
coordinates.group.startsWith("com.hivemq") && (coordinates.name != "hivemq-mqtt-client")

// defines the license to choose, if multiple licenses are available for an artifact
private val LICENSE_ORDER = listOf(
KnownLicense.APACHE_2_0,
KnownLicense.MIT,
KnownLicense.MIT_0,
KnownLicense.ZERO_BSD,
KnownLicense.UNLICENSE,
KnownLicense.BOUNCY_CASTLE,
KnownLicense.BLUE_OAK_1_0_0,
KnownLicense.ISC,
KnownLicense.BSD_3_CLAUSE,
KnownLicense.BSD_2_CLAUSE,
KnownLicense.GO,
KnownLicense.CC0_1_0,
KnownLicense.CC_BY_4_0,
KnownLicense.OFL_1_1,
KnownLicense.PUBLIC_DOMAIN,
KnownLicense.W3C_19980720,
KnownLicense.EDL_1_0,
KnownLicense.EPL_2_0,
KnownLicense.EPL_1_0,
KnownLicense.CDDL_1_1,
KnownLicense.CDDL_1_0,
KnownLicense.UNICODE_DFS_2016,
KnownLicense.LGPL_2_1_OR_LATER
)
}

@get:Input
val projectName = project.objects.property<String>()

@get:InputFile
val dependencyLicense: RegularFileProperty = project.objects.fileProperty()


@get:OutputDirectory
val outputDirectory: DirectoryProperty = project.objects.directoryProperty()

@TaskAction
protected fun run() {
val productName = projectName.get()
val dependencyLicenseFile = dependencyLicense.get().asFile.absoluteFile
val resultPlaintextFile = outputDirectory.get().asFile.resolve(productName)
val resultHtmlFile = outputDirectory.get().asFile.resolve("$productName.html")

check(productName.isNotBlank()) { "Project name is blank" }
if (resultPlaintextFile.exists()) {
check(resultPlaintextFile.delete()) { "Could not delete file '$resultPlaintextFile'" }
}
if (resultHtmlFile.exists()) {
check(resultHtmlFile.delete()) { "Could not delete file '$resultHtmlFile'" }
}

val xmlMapper = XmlMapper()
xmlMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
val dependencies = xmlMapper.readValue(dependencyLicenseFile, DependencyReport.Root())
val entries = TreeMap<String, Pair<Coordinates, KnownLicense>>()
for (dependency in dependencies) {
if (dependency.name.endsWith(".jar")) {
System.err.println("Skipping jar dependency: " + dependency.name)
continue
}

val nameParts = dependency.name.split(":")
check(nameParts.size == 3) { "Invalid dependency '${dependency.name}'" }
val coordinates = Coordinates(nameParts[0], nameParts[1], nameParts[2])
if (shouldIgnore(coordinates)) continue
val licenses = dependency.licenses.map { convertLicense(it, coordinates) }

val chosenLicense =
checkNotNull(chooseLicense(licenses)) { "[Edge Plugin] License can not be determined for '$coordinates'" }
entries[coordinates.moduleId] = Pair(coordinates, chosenLicense)
}

val licensePlaintext = StringBuilder()
val licenseHtml = StringBuilder()
licensePlaintext.addHeaderPlaintext(productName)
licenseHtml.addHeaderHtml(productName)
for ((coordinates, chosenLicense) in entries.values) {
licensePlaintext.addLinePlaintext(coordinates, chosenLicense)
licenseHtml.addLineHtml(coordinates, chosenLicense)
}
licensePlaintext.addFooterPlaintext()
licenseHtml.addFooterHtml()

resultPlaintextFile.writeText(licensePlaintext.toString())
resultHtmlFile.writeText(licenseHtml.toString())
}

private fun convertLicense(license: DependencyReport.License, coordinates: Coordinates): License {
val name = license.name
val url = license.url
return when {
name.matches(".*(Apache|APACHE).*[\\s\\-v](2\\.0.*|2(\\s.*|$))".toRegex()) -> KnownLicense.APACHE_2_0
name == "Bouncy Castle Licence" -> KnownLicense.BOUNCY_CASTLE
name == "Bouncy Castle License" -> KnownLicense.BOUNCY_CASTLE
name.matches("(.*BSD.*2.*[Cc]lause.*)|(.*2.*[Cc]lause.*BSD.*)".toRegex()) -> KnownLicense.BSD_2_CLAUSE
name.matches("(.*BSD.*3.*[Cc]lause.*)|(.*3.*[Cc]lause.*BSD.*)|(.*[Nn]ew.*BSD.*)|(.*BSD.*[Nn]ew.*)".toRegex()) || (url == "https://opensource.org/licenses/BSD-3-Clause") -> KnownLicense.BSD_3_CLAUSE
name == "CC0" -> KnownLicense.CC0_1_0
url == "https://glassfish.dev.java.net/public/CDDLv1.0.html" -> KnownLicense.CDDL_1_0
(url == "https://oss.oracle.com/licenses/CDDL+GPL-1.1") || (url == "https://github.com/javaee/javax.annotation/blob/master/LICENSE") || (url == "https://glassfish.java.net/public/CDDL+GPL_1_1.html") -> KnownLicense.CDDL_1_1
name.matches(".*(EDL|Eclipse.*Distribution.*License).*1\\.0.*".toRegex()) -> KnownLicense.EDL_1_0
name.matches(".*(EPL|Eclipse.*Public.*License).*1\\.0.*".toRegex()) -> KnownLicense.EPL_1_0
name.matches(".*(EPL|Eclipse.*Public.*License).*2\\.0.*".toRegex()) -> KnownLicense.EPL_2_0
name == "Go License" -> KnownLicense.GO
name.matches(".*MIT(\\s.*|$)".toRegex()) -> KnownLicense.MIT
name.matches(".*MIT-0.*".toRegex()) -> KnownLicense.MIT_0
name == "Public Domain" -> KnownLicense.PUBLIC_DOMAIN
url == "http://www.w3.org/Consortium/Legal/copyright-software-19980720" -> KnownLicense.W3C_19980720
// from here license name and url are not enough to determine the exact license, so we checked the specific dependency manually
(name == "BSD") && (coordinates.group == "dk.brics") && (coordinates.name == "automaton") -> KnownLicense.BSD_3_CLAUSE
(name == "BSD") && (coordinates.group == "org.picocontainer") && (coordinates.name == "picocontainer") -> KnownLicense.BSD_3_CLAUSE
(name == "BSD") && (coordinates.group == "org.ow2.asm") && (coordinates.name == "asm") -> KnownLicense.BSD_3_CLAUSE
(name == "BSD licence") && (coordinates.group == "org.antlr") && (coordinates.name == "antlr-runtime") -> KnownLicense.BSD_3_CLAUSE
(name == "The BSD License") && (coordinates.group == "org.antlr") && (coordinates.name == "ST4") -> KnownLicense.BSD_3_CLAUSE
(name == "The BSD License") && (coordinates.group == "org.codehaus.woodstox") && (coordinates.name == "stax2-api") -> KnownLicense.BSD_2_CLAUSE
(name == "Unicode/ICU License") && (coordinates.group == "com.ibm.icu") && (coordinates.name == "icu4j") && (coordinates.version == "72.1") -> KnownLicense.UNICODE_DFS_2016
(name == "LGPL-2.1") && (coordinates.group == "org.mariadb.jdbc") && (coordinates.name == "mariadb-java-client") -> KnownLicense.LGPL_2_1_OR_LATER
name == "CC-BY-4.0" -> KnownLicense.CC_BY_4_0
name == "BlueOak-1.0.0" -> KnownLicense.BLUE_OAK_1_0_0
name == "0BSD" -> KnownLicense.ZERO_BSD
name == "OFL-1.1" -> KnownLicense.OFL_1_1
name == "ISC" -> KnownLicense.ISC
name == "Unlicense" -> KnownLicense.UNLICENSE
else -> {
UnknownLicense(name, url)
}
}
}

private fun chooseLicense(licenses: List<License>): KnownLicense? {
var chosenLicense: KnownLicense? = null
var indexOfChosenLicense = Int.MAX_VALUE
for (license in licenses) {
if (license is KnownLicense) {
val indexOfLicense = LICENSE_ORDER.indexOf(license)
if ((indexOfLicense != -1) && (indexOfLicense < indexOfChosenLicense)) {
chosenLicense = license
indexOfChosenLicense = indexOfLicense
}
}
}
return chosenLicense
}

private fun StringBuilder.addHeaderPlaintext(productName: String) = append(
"""
Third Party Licenses
==============================
$productName uses the following third party libraries:
Module | Version | License ID | License URL
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
""".trimIndent()
)

private fun StringBuilder.addHeaderHtml(productName: String) = append(
"""
<head>
<title>Third Party Licences</title>
<style>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
border-spacing: 0;
}
th, td {
padding: 5px;
}
</style>
</head>
<body>
<h2>Third Party Licenses</h2>
<p>$productName uses the following third party libraries</p>
<table>
<tbody>
<tr>
<th>Module</th>
<th>Version</th>
<th>License ID</th>
<th>License URL</th>
</tr>
""".trimIndent()
)

private fun StringBuilder.addLinePlaintext(coordinates: Coordinates, license: KnownLicense) =
append(
" ${"%-74s".format(coordinates.moduleId)} | ${"%-41s".format(coordinates.version)} | ${
"%-13s".format(
license.id
)
} | ${license.url}\n"
)

private fun StringBuilder.addLineHtml(coordinates: Coordinates, license: KnownLicense) = append(
"""
| <tr>
| <td>${coordinates.moduleId}</td>
| <td>${coordinates.version}</td>
| <td>${license.id}</td>
| <td>
| <a href="${license.url}">${license.url}</a>
| </td>
| <td></td>
| </tr>
|
""".trimMargin()
)

private fun StringBuilder.addFooterPlaintext() = append(
"""
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The open source code of the libraries can be obtained by sending an email to [email protected].
""".trimIndent()
)

private fun StringBuilder.addFooterHtml() = append(
"""
</tbody>
</table>
<p>The open source code of the libraries can be obtained by sending an email to <a href="mailto:[email protected]">[email protected]</a>.
</p>
</body>
""".trimIndent()
)
}

data class Coordinates(val group: String, val name: String, val version: String) {
val moduleId get() = "$group:$name"

override fun toString() = "$group:$name:$version"
}
Loading

0 comments on commit 68dd16d

Please sign in to comment.