Skip to content

Commit 906e4c7

Browse files
DC2-DanielKruegercodepitbull
authored andcommitted
fix third party license generation
1 parent 85054fd commit 906e4c7

File tree

12 files changed

+467
-88
lines changed

12 files changed

+467
-88
lines changed

edge-plugins/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ gradlePlugin {
2525
implementationClass = "$group.versionupdater.VersionUpdaterPlugin"
2626
}
2727
}
28+
plugins {
29+
create("third-party-license-generator") {
30+
id = "$group.$name"
31+
implementationClass = "$group.licensethirdparty.ThirdPartyLicenseGeneratorPlugin"
32+
}
33+
}
2834
}
2935

3036
tasks.withType<AbstractArchiveTask>().configureEach {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.hivemq.licensethirdparty
2+
3+
import com.fasterxml.jackson.core.type.TypeReference
4+
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
5+
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
6+
7+
class DependencyReport {
8+
9+
class Root : TypeReference<List<Dependency>>()
10+
11+
class Dependency(
12+
13+
@param:JacksonXmlProperty(localName = "name", isAttribute = true)
14+
val name: String,
15+
16+
@param:JacksonXmlProperty(localName = "file")
17+
val file: String,
18+
19+
@param:JacksonXmlElementWrapper(useWrapping = false)
20+
@param:JacksonXmlProperty(localName = "license")
21+
val licenses: List<License>,
22+
)
23+
24+
class License(
25+
26+
@param:JacksonXmlProperty(localName = "name", isAttribute = true)
27+
val name: String,
28+
29+
@param:JacksonXmlProperty(localName = "url", isAttribute = true)
30+
val url: String?,
31+
)
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.hivemq.licensethirdparty
2+
3+
interface License {
4+
val fullName: String
5+
val url: String?
6+
}
7+
8+
enum class KnownLicense(val id: String, override val fullName: String, override val url: String) : License {
9+
APACHE_2_0("Apache-2.0", "Apache License 2.0", "https://spdx.org/licenses/Apache-2.0.html"),
10+
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"),
11+
BOUNCY_CASTLE("MIT", "Bouncy Castle Licence", "https://www.bouncycastle.org/licence.html"),
12+
BSD_2_CLAUSE("BSD-2-Clause", "BSD 2-Clause \"Simplified\" License", "https://spdx.org/licenses/BSD-2-Clause.html"),
13+
BSD_3_CLAUSE("BSD-3-Clause", "BSD 3-Clause \"New\" or \"Revised\" License", "https://spdx.org/licenses/BSD-3-Clause.html"),
14+
CC_BY_4_0("CC-BY-4.0", "Creative Commons Attribution 4.0 International", "https://spdx.org/licenses/CC-BY-4.0.html"),
15+
CC0_1_0("CC0-1.0", "Creative Commons Zero v1.0 Universal", "https://spdx.org/licenses/CC0-1.0.html"),
16+
CDDL_1_0("CDDL-1.0", "Common Development and Distribution License 1.0", "https://spdx.org/licenses/CDDL-1.0.html"),
17+
CDDL_1_1("CDDL-1.1", "Common Development and Distribution License 1.1", "https://spdx.org/licenses/CDDL-1.1.html"),
18+
// EDL has BSD-3-Clause as SPDX id, documented in the following links:
19+
// https://spdx.org/licenses/BSD-3-Clause.html
20+
// https://www.eclipse.org/org/documents/edl-v10.php
21+
// https://lists.spdx.org/g/Spdx-legal/topic/request_for_adding_eclipse/67981884
22+
EDL_1_0("BSD-3-Clause", "Eclipse Distribution License - v 1.0", "https://www.eclipse.org/org/documents/edl-v10.php"),
23+
EPL_1_0("EPL-1.0", "Eclipse Public License 1.0", "https://spdx.org/licenses/EPL-1.0.html"),
24+
EPL_2_0("EPL-2.0", "Eclipse Public License 2.0", "https://spdx.org/licenses/EPL-2.0.html"),
25+
GO("BSD-3-Clause", "Go License", "https://golang.org/LICENSE"),
26+
ISC("ISC", "ISC License", "https://spdx.org/licenses/ISC.html"),
27+
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"),
28+
MIT("MIT", "MIT License", "https://spdx.org/licenses/MIT.html"),
29+
MIT_0("MIT-0", "MIT No Attribution", "https://spdx.org/licenses/MIT-0.html"),
30+
OFL_1_1("OFL-1.1", "SIL Open Font License 1.1", "https://spdx.org/licenses/OFL-1.1.html"),
31+
PUBLIC_DOMAIN("Public Domain", "Public Domain", ""),
32+
UNICODE_DFS_2016("Unicode-DFS-2016", "Unicode License Agreement - Data Files and Software (2016)", "https://spdx.org/licenses/Unicode-DFS-2016.html"),
33+
UNLICENSE("Unlicense", "Unlicense Yourself: Set Your Code Free", "https://unlicense.org/"),
34+
W3C_19980720("W3C-19980720", "W3C Software Notice and License (1998-07-20)", "https://spdx.org/licenses/W3C-19980720.html"),
35+
ZERO_BSD("0BSD", "BSD Zero Clause License", "https://spdx.org/licenses/0BSD.html"),
36+
}
37+
38+
39+
data class UnknownLicense(override val fullName: String, override val url: String?) : License
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.hivemq.licensethirdparty
2+
3+
import org.gradle.api.Plugin
4+
import org.gradle.api.Project
5+
import org.gradle.kotlin.dsl.register
6+
7+
class ThirdPartyLicenseGeneratorPlugin : Plugin<Project> {
8+
9+
override fun apply(project: Project) {
10+
project.tasks.register<UpdateThirdPartyLicensesTask>("updateThirdPartyLicenses")
11+
}
12+
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package com.hivemq.licensethirdparty
2+
3+
import com.fasterxml.jackson.databind.DeserializationFeature
4+
import com.fasterxml.jackson.dataformat.xml.XmlMapper
5+
import org.gradle.api.DefaultTask
6+
import org.gradle.api.file.DirectoryProperty
7+
import org.gradle.api.file.RegularFileProperty
8+
import org.gradle.api.tasks.Input
9+
import org.gradle.api.tasks.InputFile
10+
import org.gradle.api.tasks.OutputDirectory
11+
import org.gradle.api.tasks.TaskAction
12+
import org.gradle.kotlin.dsl.property
13+
import java.util.*
14+
15+
/**
16+
* Reads the `dependency-license.xml` file created by the `downloadLicenses` task and creates `licenses` and
17+
* `licenses.html` files in the configured [outputDirectory].
18+
*/
19+
abstract class UpdateThirdPartyLicensesTask : DefaultTask() {
20+
21+
companion object {
22+
23+
// defines the artifacts that should be ignored in the third-party license report
24+
private fun shouldIgnore(coordinates: Coordinates) =
25+
coordinates.group.startsWith("com.hivemq") && (coordinates.name != "hivemq-mqtt-client")
26+
27+
// defines the license to choose, if multiple licenses are available for an artifact
28+
private val LICENSE_ORDER = listOf(
29+
KnownLicense.APACHE_2_0,
30+
KnownLicense.MIT,
31+
KnownLicense.MIT_0,
32+
KnownLicense.ZERO_BSD,
33+
KnownLicense.UNLICENSE,
34+
KnownLicense.BOUNCY_CASTLE,
35+
KnownLicense.BLUE_OAK_1_0_0,
36+
KnownLicense.ISC,
37+
KnownLicense.BSD_3_CLAUSE,
38+
KnownLicense.BSD_2_CLAUSE,
39+
KnownLicense.GO,
40+
KnownLicense.CC0_1_0,
41+
KnownLicense.CC_BY_4_0,
42+
KnownLicense.OFL_1_1,
43+
KnownLicense.PUBLIC_DOMAIN,
44+
KnownLicense.W3C_19980720,
45+
KnownLicense.EDL_1_0,
46+
KnownLicense.EPL_2_0,
47+
KnownLicense.EPL_1_0,
48+
KnownLicense.CDDL_1_1,
49+
KnownLicense.CDDL_1_0,
50+
KnownLicense.UNICODE_DFS_2016,
51+
KnownLicense.LGPL_2_1_OR_LATER
52+
)
53+
}
54+
55+
@get:Input
56+
val projectName = project.objects.property<String>()
57+
58+
@get:InputFile
59+
val dependencyLicense: RegularFileProperty = project.objects.fileProperty()
60+
61+
62+
@get:OutputDirectory
63+
val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
64+
65+
@TaskAction
66+
protected fun run() {
67+
val productName = projectName.get()
68+
val dependencyLicenseFile = dependencyLicense.get().asFile.absoluteFile
69+
val resultPlaintextFile = outputDirectory.get().asFile.resolve(productName)
70+
val resultHtmlFile = outputDirectory.get().asFile.resolve("$productName.html")
71+
72+
check(productName.isNotBlank()) { "Project name is blank" }
73+
if (resultPlaintextFile.exists()) {
74+
check(resultPlaintextFile.delete()) { "Could not delete file '$resultPlaintextFile'" }
75+
}
76+
if (resultHtmlFile.exists()) {
77+
check(resultHtmlFile.delete()) { "Could not delete file '$resultHtmlFile'" }
78+
}
79+
80+
val xmlMapper = XmlMapper()
81+
xmlMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
82+
val dependencies = xmlMapper.readValue(dependencyLicenseFile, DependencyReport.Root())
83+
val entries = TreeMap<String, Pair<Coordinates, KnownLicense>>()
84+
for (dependency in dependencies) {
85+
if (dependency.name.endsWith(".jar")) {
86+
System.err.println("Skipping jar dependency: " + dependency.name)
87+
continue
88+
}
89+
90+
val nameParts = dependency.name.split(":")
91+
check(nameParts.size == 3) { "Invalid dependency '${dependency.name}'" }
92+
val coordinates = Coordinates(nameParts[0], nameParts[1], nameParts[2])
93+
if (shouldIgnore(coordinates)) continue
94+
val licenses = dependency.licenses.map { convertLicense(it, coordinates) }
95+
96+
val chosenLicense =
97+
checkNotNull(chooseLicense(licenses)) { "[Edge Plugin] License can not be determined for '$coordinates'" }
98+
entries[coordinates.moduleId] = Pair(coordinates, chosenLicense)
99+
}
100+
101+
val licensePlaintext = StringBuilder()
102+
val licenseHtml = StringBuilder()
103+
licensePlaintext.addHeaderPlaintext(productName)
104+
licenseHtml.addHeaderHtml(productName)
105+
for ((coordinates, chosenLicense) in entries.values) {
106+
licensePlaintext.addLinePlaintext(coordinates, chosenLicense)
107+
licenseHtml.addLineHtml(coordinates, chosenLicense)
108+
}
109+
licensePlaintext.addFooterPlaintext()
110+
licenseHtml.addFooterHtml()
111+
112+
resultPlaintextFile.writeText(licensePlaintext.toString())
113+
resultHtmlFile.writeText(licenseHtml.toString())
114+
}
115+
116+
private fun convertLicense(license: DependencyReport.License, coordinates: Coordinates): License {
117+
val name = license.name
118+
val url = license.url
119+
return when {
120+
name.matches(".*(Apache|APACHE).*[\\s\\-v](2\\.0.*|2(\\s.*|$))".toRegex()) -> KnownLicense.APACHE_2_0
121+
name == "Bouncy Castle Licence" -> KnownLicense.BOUNCY_CASTLE
122+
name == "Bouncy Castle License" -> KnownLicense.BOUNCY_CASTLE
123+
name.matches("(.*BSD.*2.*[Cc]lause.*)|(.*2.*[Cc]lause.*BSD.*)".toRegex()) -> KnownLicense.BSD_2_CLAUSE
124+
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
125+
name == "CC0" -> KnownLicense.CC0_1_0
126+
url == "https://glassfish.dev.java.net/public/CDDLv1.0.html" -> KnownLicense.CDDL_1_0
127+
(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
128+
name.matches(".*(EDL|Eclipse.*Distribution.*License).*1\\.0.*".toRegex()) -> KnownLicense.EDL_1_0
129+
name.matches(".*(EPL|Eclipse.*Public.*License).*1\\.0.*".toRegex()) -> KnownLicense.EPL_1_0
130+
name.matches(".*(EPL|Eclipse.*Public.*License).*2\\.0.*".toRegex()) -> KnownLicense.EPL_2_0
131+
name == "Go License" -> KnownLicense.GO
132+
name.matches(".*MIT(\\s.*|$)".toRegex()) -> KnownLicense.MIT
133+
name.matches(".*MIT-0.*".toRegex()) -> KnownLicense.MIT_0
134+
name == "Public Domain" -> KnownLicense.PUBLIC_DOMAIN
135+
url == "http://www.w3.org/Consortium/Legal/copyright-software-19980720" -> KnownLicense.W3C_19980720
136+
// from here license name and url are not enough to determine the exact license, so we checked the specific dependency manually
137+
(name == "BSD") && (coordinates.group == "dk.brics") && (coordinates.name == "automaton") -> KnownLicense.BSD_3_CLAUSE
138+
(name == "BSD") && (coordinates.group == "org.picocontainer") && (coordinates.name == "picocontainer") -> KnownLicense.BSD_3_CLAUSE
139+
(name == "BSD") && (coordinates.group == "org.ow2.asm") && (coordinates.name == "asm") -> KnownLicense.BSD_3_CLAUSE
140+
(name == "BSD licence") && (coordinates.group == "org.antlr") && (coordinates.name == "antlr-runtime") -> KnownLicense.BSD_3_CLAUSE
141+
(name == "The BSD License") && (coordinates.group == "org.antlr") && (coordinates.name == "ST4") -> KnownLicense.BSD_3_CLAUSE
142+
(name == "The BSD License") && (coordinates.group == "org.codehaus.woodstox") && (coordinates.name == "stax2-api") -> KnownLicense.BSD_2_CLAUSE
143+
(name == "Unicode/ICU License") && (coordinates.group == "com.ibm.icu") && (coordinates.name == "icu4j") && (coordinates.version == "72.1") -> KnownLicense.UNICODE_DFS_2016
144+
(name == "LGPL-2.1") && (coordinates.group == "org.mariadb.jdbc") && (coordinates.name == "mariadb-java-client") -> KnownLicense.LGPL_2_1_OR_LATER
145+
name == "CC-BY-4.0" -> KnownLicense.CC_BY_4_0
146+
name == "BlueOak-1.0.0" -> KnownLicense.BLUE_OAK_1_0_0
147+
name == "0BSD" -> KnownLicense.ZERO_BSD
148+
name == "OFL-1.1" -> KnownLicense.OFL_1_1
149+
name == "ISC" -> KnownLicense.ISC
150+
name == "Unlicense" -> KnownLicense.UNLICENSE
151+
else -> {
152+
UnknownLicense(name, url)
153+
}
154+
}
155+
}
156+
157+
private fun chooseLicense(licenses: List<License>): KnownLicense? {
158+
var chosenLicense: KnownLicense? = null
159+
var indexOfChosenLicense = Int.MAX_VALUE
160+
for (license in licenses) {
161+
if (license is KnownLicense) {
162+
val indexOfLicense = LICENSE_ORDER.indexOf(license)
163+
if ((indexOfLicense != -1) && (indexOfLicense < indexOfChosenLicense)) {
164+
chosenLicense = license
165+
indexOfChosenLicense = indexOfLicense
166+
}
167+
}
168+
}
169+
return chosenLicense
170+
}
171+
172+
private fun StringBuilder.addHeaderPlaintext(productName: String) = append(
173+
"""
174+
Third Party Licenses
175+
==============================
176+
177+
$productName uses the following third party libraries:
178+
179+
Module | Version | License ID | License URL
180+
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
181+
182+
""".trimIndent()
183+
)
184+
185+
private fun StringBuilder.addHeaderHtml(productName: String) = append(
186+
"""
187+
<head>
188+
<title>Third Party Licences</title>
189+
<style>
190+
table, th, td {
191+
border: 1px solid black;
192+
border-collapse: collapse;
193+
border-spacing: 0;
194+
}
195+
196+
th, td {
197+
padding: 5px;
198+
}
199+
</style>
200+
</head>
201+
202+
<body>
203+
204+
<h2>Third Party Licenses</h2>
205+
<p>$productName uses the following third party libraries</p>
206+
207+
<table>
208+
<tbody>
209+
<tr>
210+
<th>Module</th>
211+
<th>Version</th>
212+
<th>License ID</th>
213+
<th>License URL</th>
214+
</tr>
215+
216+
""".trimIndent()
217+
)
218+
219+
private fun StringBuilder.addLinePlaintext(coordinates: Coordinates, license: KnownLicense) =
220+
append(
221+
" ${"%-74s".format(coordinates.moduleId)} | ${"%-41s".format(coordinates.version)} | ${
222+
"%-13s".format(
223+
license.id
224+
)
225+
} | ${license.url}\n"
226+
)
227+
228+
private fun StringBuilder.addLineHtml(coordinates: Coordinates, license: KnownLicense) = append(
229+
"""
230+
| <tr>
231+
| <td>${coordinates.moduleId}</td>
232+
| <td>${coordinates.version}</td>
233+
| <td>${license.id}</td>
234+
| <td>
235+
| <a href="${license.url}">${license.url}</a>
236+
| </td>
237+
| <td></td>
238+
| </tr>
239+
|
240+
""".trimMargin()
241+
)
242+
243+
private fun StringBuilder.addFooterPlaintext() = append(
244+
"""
245+
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
246+
247+
The open source code of the libraries can be obtained by sending an email to [email protected].
248+
249+
""".trimIndent()
250+
)
251+
252+
private fun StringBuilder.addFooterHtml() = append(
253+
"""
254+
</tbody>
255+
</table>
256+
<p>The open source code of the libraries can be obtained by sending an email to <a href="mailto:[email protected]">[email protected]</a>.
257+
</p>
258+
</body>
259+
260+
""".trimIndent()
261+
)
262+
}
263+
264+
data class Coordinates(val group: String, val name: String, val version: String) {
265+
val moduleId get() = "$group:$name"
266+
267+
override fun toString() = "$group:$name:$version"
268+
}

0 commit comments

Comments
 (0)