|
| 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