diff --git a/README.md b/README.md index f46f9db..b6351d4 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,9 @@ Change log v1.78 * chore: bump Gradle 6.7 -> 6.9.1 * license-gather: ignore xml namespaces when parsing POM files (see #43) +* license-gather: fix license inference from Bundle-License manifest attribute (see #48) + +Thanks to [Florian Dreier](https://github.com/DreierF) for identifying bugs and suggesting fixes. v1.77 * crlf-plugin: bump jgit to 5.13.0.202109080827-r diff --git a/buildSrc/subprojects/license-texts/src/main/kotlin/com/github/vlsi/gradle/license/EnumGeneratorTask.kt b/buildSrc/subprojects/license-texts/src/main/kotlin/com/github/vlsi/gradle/license/EnumGeneratorTask.kt index eb76d87..41166a2 100644 --- a/buildSrc/subprojects/license-texts/src/main/kotlin/com/github/vlsi/gradle/license/EnumGeneratorTask.kt +++ b/buildSrc/subprojects/license-texts/src/main/kotlin/com/github/vlsi/gradle/license/EnumGeneratorTask.kt @@ -147,6 +147,16 @@ open class EnumGeneratorTask @Inject constructor(objectFactory: ObjectFactory) : .initializer("values().associateBy { it.id }") .build() ) + .addProperty( + PropertySpec.builder( + "uriToInstance", + Map::class.asClassName() + .parameterizedBy(URI::class.asClassName(), className), + KModifier.PRIVATE + ) + .initializer("values().flatMap { e -> (e.uri + e.detailsUri).map { it to e } }.toMap()") + .build() + ) .addFunction( FunSpec.builder("fromId") .addParameter("id", String::class) @@ -159,6 +169,30 @@ open class EnumGeneratorTask @Inject constructor(objectFactory: ObjectFactory) : .addStatement("return idToInstance[id]") .build() ) + .addFunction( + FunSpec.builder("fromUri") + .addParameter("uri", URI::class) + .addStatement( + "return fromUriOrNull(uri) ?: throw %T(%P)", + NoSuchElementException::class, + "No license found for given URI: \$uri" + ) + .build() + ) + .addFunction( + FunSpec.builder("fromUriOrNull") + .addParameter("uri", URI::class) + .addStatement("return uriToInstance[uri.toHttps()]") + .build() + ) + .addFunction( + FunSpec.builder("toHttps") + .addModifiers(KModifier.PRIVATE) + .receiver(URI::class) + .returns(URI::class) + .addStatement("return if (!toString().startsWith(\"http://\")) this else URI(toString().replaceFirst(\"http:\", \"https:\"))") + .build() + ) .build() ) .addProperty( @@ -179,10 +213,21 @@ open class EnumGeneratorTask @Inject constructor(objectFactory: ObjectFactory) : TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter("%S", it.id) .addSuperclassConstructorParameter("%S", it.name) - .addSuperclassConstructorParameter("%S", it.detailsUrl) - .addSuperclassConstructorParameter("arrayOf(%L)", - it.seeAlso.map { url -> CodeBlock.of("%S", url.trim()) } - .joinToCode(", ")) + .addSuperclassConstructorParameter( + "%S", + it.detailsUrl.replaceFirst("http://", "https://") + ) + .addSuperclassConstructorParameter( + "arrayOf(%L)", + it.seeAlso + .map { url -> + CodeBlock.of( + "%S", + url.trim().replaceFirst("http://", "https://") + ) + } + .joinToCode(", ") + ) .build() ) } diff --git a/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/GatherLicenseTask.kt b/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/GatherLicenseTask.kt index cf6056d..f6bc8e5 100644 --- a/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/GatherLicenseTask.kt +++ b/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/GatherLicenseTask.kt @@ -22,6 +22,8 @@ import com.github.vlsi.gradle.license.api.JustLicense import com.github.vlsi.gradle.license.api.License import com.github.vlsi.gradle.license.api.LicenseExpression import com.github.vlsi.gradle.license.api.LicenseExpressionParser +import com.github.vlsi.gradle.license.api.OsgiBundleLicenseParser +import com.github.vlsi.gradle.license.api.ParseException import com.github.vlsi.gradle.license.api.SpdxLicense import com.github.vlsi.gradle.license.api.asExpression import com.github.vlsi.gradle.license.api.text @@ -471,6 +473,9 @@ open class GatherLicenseTask @Inject constructor( detectedLicenses: MutableMap, licenseExpressionParser: LicenseExpressionParser ) { + val bundleLicenseParser = OsgiBundleLicenseParser(licenseExpressionParser) { + SpdxLicense.fromUriOrNull(it)?.asExpression() + } for (e in detectedLicenses) { if (e.value.license != null) { continue @@ -484,7 +489,7 @@ open class GatherLicenseTask @Inject constructor( ) continue } - if (!file.endsWith(".jar")) { + if (file.extension != "jar") { logger.debug( "File {} for artifact {} does not look like a JAR. Will skip MANIFEST.MF check", file, @@ -500,10 +505,8 @@ open class GatherLicenseTask @Inject constructor( ) JarFile(file).use { jar -> val bundleLicense = jar.manifest.mainAttributes.getValue("Bundle-License") - val license = bundleLicense?.substringBefore(";")?.let { - licenseExpressionParser.parse(it) - } - if (license != null) { + ?: return@use + bundleLicenseParser.parseOrNull(bundleLicense, file)?.let { license -> logger.debug("Detected license for ${e.key}: $license") e.setValue(e.value.copy(license = license)) } diff --git a/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/api/OsgiBundleLicenseParser.kt b/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/api/OsgiBundleLicenseParser.kt new file mode 100644 index 0000000..46e6e1d --- /dev/null +++ b/plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/api/OsgiBundleLicenseParser.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.github.vlsi.gradle.license.api + +import org.slf4j.LoggerFactory +import java.net.URI +import java.net.URISyntaxException + +class OsgiBundleLicenseParser( + private val licenseExpressionParser: LicenseExpressionParser, + private val lookupLicenseByUri: (URI) -> LicenseExpression? +) { + private val logger = LoggerFactory.getLogger(OsgiBundleLicenseParser::class.java) + + fun parseOrNull(bundleLicense: String, context: Any): LicenseExpression? { + return if (bundleLicense.contains(',')) { + logger.info( + "Ignoring Bundle-License '{}' in {} since it contains multiple license references", + bundleLicense, + context + ) + null + } else if (bundleLicense.startsWith("http")) { + // Infer license from the URI + val uri = try { + URI(bundleLicense) + } catch (e: URISyntaxException) { + logger.info( + "Invalid URI for license in Bundle-License value '{}' in {}", + bundleLicense, + context, + e + ) + return null + } + lookupLicenseByUri(uri) + } else { + // Infer license from "SPDX-Expression; licenseURI" + // We ignore URI as the expression should be more important + bundleLicense.substringBefore(";").let { + try { + licenseExpressionParser.parse(it) + } catch (e: ParseException) { + logger.info( + "Unable to parse Bundle-License value '{}' in {}", + bundleLicense, + context, + e + ) + null + } + } + } + } +} diff --git a/plugins/license-gather-plugin/src/test/kotlin/com/github/vlsi/gradle/license/OsgiBundleLicenseParserTest.kt b/plugins/license-gather-plugin/src/test/kotlin/com/github/vlsi/gradle/license/OsgiBundleLicenseParserTest.kt new file mode 100644 index 0000000..861e0f1 --- /dev/null +++ b/plugins/license-gather-plugin/src/test/kotlin/com/github/vlsi/gradle/license/OsgiBundleLicenseParserTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Vladimir Sitnikov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.github.vlsi.gradle.license + +import com.github.vlsi.gradle.license.api.LicenseExpressionParser +import com.github.vlsi.gradle.license.api.OsgiBundleLicenseParser +import com.github.vlsi.gradle.license.api.SpdxLicense +import com.github.vlsi.gradle.license.api.asExpression +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class OsgiBundleLicenseParserTest { + @ParameterizedTest + @CsvSource( + "license from https uri^Apache-2.0^https://www.apache.org/licenses/LICENSE-2.0", + "license from http uri^Apache-2.0^http://www.apache.org/licenses/LICENSE-2.0", + "license from https uri^Apache-1.0^https://www.apache.org/licenses/LICENSE-1.0", + "license from SPDX^Apache-2.0^Apache-2.0;https://www.apache.org/licenses/LICENSE-1.0", + "SPDX expression^Apache-1.0 OR Apache-2.0^Apache-2.0 OR Apache-1.0;https://www.apache.org/licenses/LICENSE-1.0", + delimiter = '^' + ) + fun success(comment: String, expected: String, input: String) { + val parser = OsgiBundleLicenseParser(LicenseExpressionParser()) { + SpdxLicense.fromUriOrNull(it)?.asExpression() + } + assertEquals(expected, parser.parseOrNull(input, "test input").toString()) { + "$comment, input: $input" + } + } + + @ParameterizedTest + @CsvSource( + "unknown uri^https://www.apache.org/licenses/LICENSE-1.2", + "invalid expression^Apache OR;http://www.apache.org/licenses/LICENSE-2.0", + "multiple licenses^license1,license2", + "multiple licenses^Apache-2.0;https://www.apache.org/licenses/LICENSE-2.0,Apache_1.0;https://www.apache.org/licenses/LICENSE-1.0", + delimiter = '^' + ) + fun fail(comment: String, input: String) { + val parser = OsgiBundleLicenseParser(LicenseExpressionParser()) { + SpdxLicense.fromUriOrNull(it)?.asExpression() + } + assertNull(parser.parseOrNull(input, "test input")) { + "$comment should cause OsgiBundleLicenseParser.parse failure, input: $input" + } + } +}